blob: b5efe22af6d186a0f62778f00c3cd6d13700a686 [file] [log] [blame]
/*
* 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 static com.google.common.util.concurrent.Futures.immediateVoidFuture;
import static java.lang.Math.min;
import android.content.Context;
import android.net.Uri;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import com.google.android.libraries.mobiledatadownload.Flags;
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.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.internal.annotations.SequentialControlExecutor;
import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil;
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
/**
* A class that handles of the logic for file group expiration and file expiration. Expiration is
* determined by two sources: 1) when the active_expiration_date (set server-side by the client) has
* passed 2) when stale_lifetime_secs has passed since the group became stale.
*/
public class ExpirationHandler {
private static final String TAG = "ExpirationHandler";
@VisibleForTesting
static final String MDD_EXPIRATION_HANDLER = "gms_icing_mdd_expiration_handler";
private final Context context;
private final FileGroupsMetadata fileGroupsMetadata;
private final SharedFileManager sharedFileManager;
private final SharedFilesMetadata sharedFilesMetadata;
private final EventLogger eventLogger;
private final TimeSource timeSource;
private final SynchronousFileStorage fileStorage;
private final Optional<String> instanceId;
private final SilentFeedback silentFeedback;
private final Executor sequentialControlExecutor;
private final Flags flags;
@Inject
public ExpirationHandler(
@ApplicationContext Context context,
FileGroupsMetadata fileGroupsMetadata,
SharedFileManager sharedFileManager,
SharedFilesMetadata sharedFilesMetadata,
EventLogger eventLogger,
TimeSource timeSource,
SynchronousFileStorage fileStorage,
@InstanceId Optional<String> instanceId,
SilentFeedback silentFeedback,
@SequentialControlExecutor Executor sequentialControlExecutor,
Flags flags) {
this.context = context;
this.fileGroupsMetadata = fileGroupsMetadata;
this.sharedFileManager = sharedFileManager;
this.sharedFilesMetadata = sharedFilesMetadata;
this.eventLogger = eventLogger;
this.timeSource = timeSource;
this.fileStorage = fileStorage;
this.instanceId = instanceId;
this.silentFeedback = silentFeedback;
this.sequentialControlExecutor = sequentialControlExecutor;
this.flags = flags;
}
// TODO(b/124072754): Change to package private once all code is refactored.
public ListenableFuture<Void> updateExpiration() {
return PropagatedFutures.transformAsync(
removeExpiredStaleGroups(),
voidArg0 ->
PropagatedFutures.transformAsync(
removeExpiredFreshGroups(),
voidArg1 -> removeUnaccountedFiles(),
sequentialControlExecutor),
sequentialControlExecutor);
}
/** Returns a future that checks all File Groups and remove expired ones from FileGroupManager */
private ListenableFuture<Void> removeExpiredFreshGroups() {
return PropagatedFutures.transformAsync(
fileGroupsMetadata.getAllFreshGroups(),
groups -> {
List<GroupKey> expiredGroupKeys = new ArrayList<>();
for (Pair<GroupKey, DataFileGroupInternal> pair : groups) {
GroupKey groupKey = pair.first;
DataFileGroupInternal dataFileGroup = pair.second;
Long groupExpirationDateMillis = FileGroupUtil.getExpirationDateMillis(dataFileGroup);
LogUtil.d(
"%s: Checking group %s with expiration date %s",
TAG, dataFileGroup.getGroupName(), groupExpirationDateMillis);
if (FileGroupUtil.isExpired(groupExpirationDateMillis, timeSource)) {
eventLogger.logEventSampled(
0,
dataFileGroup.getGroupName(),
dataFileGroup.getFileGroupVersionNumber(),
dataFileGroup.getBuildId(),
dataFileGroup.getVariantId());
LogUtil.d(
"%s: Expired group %s with expiration date %s",
TAG, dataFileGroup.getGroupName(), groupExpirationDateMillis);
expiredGroupKeys.add(groupKey);
// Remove Isolated structure if necessary.
if (FileGroupUtil.isIsolatedStructureAllowed(dataFileGroup)) {
FileGroupUtil.removeIsolatedFileStructure(
context, instanceId, dataFileGroup, fileStorage);
}
}
}
return PropagatedFutures.transform(
fileGroupsMetadata.removeAllGroupsWithKeys(expiredGroupKeys),
removeSuccess -> {
if (!removeSuccess) {
eventLogger.logEventSampled(0);
LogUtil.e("%s: Failed to remove expired groups!", TAG);
}
return null;
},
sequentialControlExecutor);
},
sequentialControlExecutor);
}
/** Check and update all stale File Groups; remove staled ones */
private ListenableFuture<Void> removeExpiredStaleGroups() {
return PropagatedFutures.transformAsync(
fileGroupsMetadata.getAllStaleGroups(),
staleGroups -> {
List<DataFileGroupInternal> nonExpiredStaleGroups = new ArrayList<>();
for (DataFileGroupInternal staleGroup : staleGroups) {
long groupStaleExpirationDateMillis =
FileGroupUtil.getStaleExpirationDateMillis(staleGroup);
long groupExpirationDateMillis = FileGroupUtil.getExpirationDateMillis(staleGroup);
long actualExpirationDateMillis =
min(groupStaleExpirationDateMillis, groupExpirationDateMillis);
// Remove the group from this list if its expired.
if (FileGroupUtil.isExpired(actualExpirationDateMillis, timeSource)) {
eventLogger.logEventSampled(
0,
staleGroup.getGroupName(),
staleGroup.getFileGroupVersionNumber(),
staleGroup.getBuildId(),
staleGroup.getVariantId());
// Remove Isolated structure if necessary.
if (FileGroupUtil.isIsolatedStructureAllowed(staleGroup)) {
FileGroupUtil.removeIsolatedFileStructure(
context, instanceId, staleGroup, fileStorage);
}
} else {
nonExpiredStaleGroups.add(staleGroup);
}
}
// Empty the list of stale groups in the FGGC and write only the non-expired stale groups.
return PropagatedFutures.transformAsync(
fileGroupsMetadata.removeAllStaleGroups(),
voidArg ->
PropagatedFutures.transformAsync(
fileGroupsMetadata.writeStaleGroups(nonExpiredStaleGroups),
writeSuccess -> {
if (!writeSuccess) {
eventLogger.logEventSampled(0);
LogUtil.e("%s: Failed to write back stale groups!", TAG);
}
return immediateVoidFuture();
},
sequentialControlExecutor),
sequentialControlExecutor);
},
sequentialControlExecutor);
}
private ListenableFuture<Void> removeUnaccountedFiles() {
return PropagatedFutures.transformAsync(
getFileKeysReferencedByAnyGroup(),
// Remove all shared file metadata that are not referenced by any group.
fileKeysReferencedByAnyGroup ->
PropagatedFutures.transformAsync(
sharedFilesMetadata.getAllFileKeys(),
allFileKeys -> {
List<Uri> filesRequiredByMdd = new ArrayList<>();
List<Uri> androidSharedFilesToBeReleased = new ArrayList<>();
// Use AtomicInteger because variables captured by lambdas must be effectively
// final.
AtomicInteger removedMetadataCount = new AtomicInteger(0);
List<ListenableFuture<Void>> futures = new ArrayList<>();
for (NewFileKey newFileKey : allFileKeys) {
if (!fileKeysReferencedByAnyGroup.contains(newFileKey)) {
ListenableFuture<Void> removeEntryFuture =
PropagatedFutures.transformAsync(
sharedFilesMetadata.read(newFileKey),
sharedFile -> {
if (sharedFile != null && sharedFile.getAndroidShared()) {
androidSharedFilesToBeReleased.add(
DirectoryUtil.getBlobUri(
context, sharedFile.getAndroidSharingChecksum()));
}
return PropagatedFutures.transform(
sharedFileManager.removeFileEntry(newFileKey),
success -> {
if (success) {
removedMetadataCount.getAndIncrement();
} else {
eventLogger.logEventSampled(0);
LogUtil.e(
"%s: Unsubscribe from file %s failed!",
TAG, newFileKey);
}
return null;
},
sequentialControlExecutor);
},
sequentialControlExecutor);
futures.add(removeEntryFuture);
} else {
futures.add(
PropagatedFutures.transform(
sharedFileManager.getOnDeviceUri(newFileKey),
uri -> {
if (uri != null) {
filesRequiredByMdd.add(uri);
}
return null;
},
sequentialControlExecutor));
}
}
// If isolated structure verification is enabled, include all individual isolated
// file uris referenced by fresh groups. This ensures any unaccounted isolated
// file uris are removed (i.e. verification is performed).
if (flags.enableIsolatedStructureVerification()) {
futures.add(
PropagatedFutures.transform(
getIsolatedFileUrisReferencedByFreshGroups(),
referencedIsolatedFileUris -> {
filesRequiredByMdd.addAll(referencedIsolatedFileUris);
return null;
},
sequentialControlExecutor));
} else {
// Isolated structure verification is disabled, include the base symlink
// directory as required so all isolated file uris under this directory are
// _not_ removed (i.e. verification is not performed).
filesRequiredByMdd.add(
DirectoryUtil.getBaseDownloadSymlinkDirectory(context, instanceId));
}
return PropagatedFutures.whenAllComplete(futures)
.call(
() -> {
if (removedMetadataCount.get() > 0) {
eventLogger.logMddDataDownloadFileExpirationEvent(
0, removedMetadataCount.get());
}
Uri parentDirectory =
DirectoryUtil.getBaseDownloadDirectory(context, instanceId);
int releasedFiles =
releaseUnaccountedAndroidSharedFiles(
androidSharedFilesToBeReleased);
LogUtil.d(
"%s: Total %d unaccounted file released. ", TAG, releasedFiles);
int unaccountedFileCount =
deleteUnaccountedFilesRecursively(
parentDirectory, filesRequiredByMdd);
LogUtil.d(
"%s: Total %d unaccounted file deleted. ",
TAG, unaccountedFileCount);
if (unaccountedFileCount > 0) {
eventLogger.logMddDataDownloadFileExpirationEvent(
0, unaccountedFileCount);
}
if (releasedFiles > 0) {
eventLogger.logMddDataDownloadFileExpirationEvent(0, releasedFiles);
}
return null;
},
sequentialControlExecutor);
},
sequentialControlExecutor),
sequentialControlExecutor);
}
private ListenableFuture<Set<NewFileKey>> getFileKeysReferencedByAnyGroup() {
return PropagatedFutures.transformAsync(
fileGroupsMetadata.getAllFreshGroups(),
allGroupsByKey -> {
Set<NewFileKey> fileKeysReferencedByAnyGroup = new HashSet<>();
List<DataFileGroupInternal> dataFileGroups = new ArrayList<>();
for (Pair<GroupKey, DataFileGroupInternal> dataFileGroupPair : allGroupsByKey) {
dataFileGroups.add(dataFileGroupPair.second);
}
return PropagatedFutures.transform(
fileGroupsMetadata.getAllStaleGroups(),
staleGroups -> {
dataFileGroups.addAll(staleGroups);
for (DataFileGroupInternal dataFileGroup : dataFileGroups) {
for (DataFile dataFile : dataFileGroup.getFileList()) {
fileKeysReferencedByAnyGroup.add(
SharedFilesMetadata.createKeyFromDataFileForCurrentVersion(
context,
dataFile,
dataFileGroup.getAllowedReadersEnum(),
silentFeedback));
}
}
return fileKeysReferencedByAnyGroup;
},
sequentialControlExecutor);
},
sequentialControlExecutor);
}
/**
* Get all isolated file uris that are referenced by any fresh groups.
*
* <p>Fresh groups are active/pending groups. Isolated file uris are expected when 1) the OS
* version supports symlinks (at least Lollipop (21)); and 2) The file group enables file
* isolation.
*
* @return ListenableFuture that resolves with List of isolated uris that are referenced by
* active/pending groups
*/
private ListenableFuture<List<Uri>> getIsolatedFileUrisReferencedByFreshGroups() {
List<Uri> referencedIsolatedFileUris = new ArrayList<>();
return PropagatedFutures.transform(
fileGroupsMetadata.getAllFreshGroups(),
groupKeyAndGroupList -> {
for (Pair<GroupKey, DataFileGroupInternal> groupKeyAndGroup : groupKeyAndGroupList) {
DataFileGroupInternal freshGroup = groupKeyAndGroup.second;
// Skip any groups that don't support isolated structures
if (!FileGroupUtil.isIsolatedStructureAllowed(freshGroup)) {
continue;
}
// Add the expected isolated file uris for each file
for (DataFile file : freshGroup.getFileList()) {
Uri isolatedFileUri =
FileGroupUtil.getIsolatedFileUri(context, instanceId, file, freshGroup);
referencedIsolatedFileUris.add(isolatedFileUri);
}
}
return referencedIsolatedFileUris;
},
sequentialControlExecutor);
}
private int releaseUnaccountedAndroidSharedFiles(List<Uri> androidSharedFilesToBeReleased) {
int releasedFiles = 0;
for (Uri sharedFile : androidSharedFilesToBeReleased) {
try {
fileStorage.deleteFile(sharedFile);
releasedFiles += 1;
eventLogger.logEventSampled(0);
} catch (IOException e) {
eventLogger.logEventSampled(0);
LogUtil.e(e, "%s: Failed to release unaccounted file!", TAG);
}
}
return releasedFiles;
}
// TODO(b/119622504) Fix nullness violation: incompatible types in argument.
@SuppressWarnings("nullness:argument")
private int deleteUnaccountedFilesRecursively(Uri directory, List<Uri> filesRequiredByMdd) {
int unaccountedFileCount = 0;
try {
if (!fileStorage.exists(directory)) {
return unaccountedFileCount;
}
for (Uri uri : fileStorage.children(directory)) {
try {
if (isContainedInUriList(uri, filesRequiredByMdd)) {
continue;
}
if (fileStorage.isDirectory(uri)) {
unaccountedFileCount += deleteUnaccountedFilesRecursively(uri, filesRequiredByMdd);
} else {
LogUtil.d("%s: Deleted unaccounted file with uri %s!", TAG, uri.getPath());
fileStorage.deleteFile(uri);
unaccountedFileCount++;
}
} catch (IOException e) {
eventLogger.logEventSampled(0);
LogUtil.e(e, "%s: Failed to delete unaccounted file!", TAG);
}
}
} catch (IOException e) {
eventLogger.logEventSampled(0);
LogUtil.e(e, "%s: Failed to delete unaccounted file!", TAG);
}
return unaccountedFileCount;
}
/**
* Returns true if given uri is within the given uri list or is a child of any uri in the list.
*
* <p>Used by MDD's unaccounted file logic to filter out files that shouldn't be deleted. This is
* used in two cases:
*
* <ul>
* <li>files referred by any active MDD files. This includes internal MDD files, such as delta
* files of a full active file, which are stored using the active file name and a checksum
* suffix.
* <li>symlinks created for an isolated file structure. These symlinks will reference active
* files and their lifecycle is managed on the file group level, rather than as individual
* files.
* </ul>
*/
private boolean isContainedInUriList(Uri uri, List<Uri> uriList) {
for (Uri activeUri : uriList) {
if (uri.toString().startsWith(activeUri.toString())) {
return true;
}
}
return false;
}
}