/* * 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.util; import android.content.Context; import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import com.google.android.libraries.mobiledatadownload.TimeSource; import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; import com.google.android.libraries.mobiledatadownload.file.openers.RecursiveDeleteOpener; import com.google.android.libraries.mobiledatadownload.internal.MddConstants; import com.google.common.base.Ascii; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.google.mobiledatadownload.TransformProto.Transform; import com.google.mobiledatadownload.internal.MetadataProto.DataFile; import com.google.mobiledatadownload.internal.MetadataProto.DataFile.AndroidSharingType; import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupBookkeeping; import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal; import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; import java.io.IOException; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** A collection of util methods for interaction with a DataFileGroup proto. */ public class FileGroupUtil { /** * @return the expiration date of this active file group in millis or Long.MAX_VALUE if no * expiration date is set. */ public static long getExpirationDateMillis(DataFileGroupInternal fileGroup) { return (fileGroup.getExpirationDateSecs() == 0) ? Long.MAX_VALUE : TimeUnit.SECONDS.toMillis(fileGroup.getExpirationDateSecs()); } /** Returns the expiration date of this stale file group in millis. */ public static long getStaleExpirationDateMillis(DataFileGroupInternal fileGroup) { return TimeUnit.SECONDS.toMillis(fileGroup.getBookkeeping().getStaleExpirationDate()); } /** * @return if the active group's expiration date has passed. False if no expiration date is set. */ public static boolean isActiveGroupExpired( DataFileGroupInternal fileGroup, TimeSource timeSource) { return isExpired(getExpirationDateMillis(fileGroup), timeSource); } /** * @param expirationDateMillis the date (in millis since epoch) at which expiration should occur. * @return if expirationDate has passed. */ public static boolean isExpired(long expirationDateMillis, TimeSource timeSource) { return expirationDateMillis <= timeSource.currentTimeMillis(); } /** * Returns file group key which uniquely identify a file group. * * @param groupName The file group name * @param ownerPackage File Group owner package. For legacy reasons, this might not be set in some * groups. */ public static GroupKey createGroupKey(String groupName, @Nullable String ownerPackage) { GroupKey.Builder groupKey = GroupKey.newBuilder().setGroupName(groupName); if (Strings.isNullOrEmpty(ownerPackage)) { groupKey.setOwnerPackage(MddConstants.GMS_PACKAGE); } else { groupKey.setOwnerPackage(ownerPackage); } return groupKey.build(); } /** * Returns the DataFile within dataFileGroup with the matching fileId. null if the group no such * DataFile exists. */ @Nullable public static DataFile getFileFromGroupWithId( @Nullable DataFileGroupInternal dataFileGroup, String fileId) { if (dataFileGroup == null) { return null; } for (DataFile dataFile : dataFileGroup.getFileList()) { if (fileId.equals(dataFile.getFileId())) { return dataFile; } } return null; } public static DataFileGroupInternal setStaleExpirationDate( DataFileGroupInternal dataFileGroup, long timeSinceEpoch) { DataFileGroupBookkeeping bookkeeping = dataFileGroup.getBookkeeping().toBuilder().setStaleExpirationDate(timeSinceEpoch).build(); dataFileGroup = dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build(); return dataFileGroup; } public static DataFileGroupInternal setGroupNewFilesReceivedTimestamp( DataFileGroupInternal dataFileGroup, long timeSinceEpoch) { DataFileGroupBookkeeping bookkeeping = dataFileGroup.getBookkeeping().toBuilder() .setGroupNewFilesReceivedTimestamp(timeSinceEpoch) .build(); dataFileGroup = dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build(); return dataFileGroup; } /** Sets the given downloaded timestamp in the given group */ public static DataFileGroupInternal setDownloadedTimestampInMillis( DataFileGroupInternal dataFileGroup, long timeSinceEpochInMillis) { DataFileGroupBookkeeping bookkeeping = dataFileGroup.getBookkeeping().toBuilder() .setGroupDownloadedTimestampInMillis(timeSinceEpochInMillis) .build(); dataFileGroup = dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build(); return dataFileGroup; } /** Sets the given download started timestamp in the given group. */ public static DataFileGroupInternal setDownloadStartedTimestampInMillis( DataFileGroupInternal dataFileGroup, long timeSinceEpochInMillis) { DataFileGroupBookkeeping bookkeeping = dataFileGroup.getBookkeeping().toBuilder() .setGroupDownloadStartedTimestampInMillis(timeSinceEpochInMillis) .build(); dataFileGroup = dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build(); return dataFileGroup; } /** Sets the isolated root if the file group supports isolated structures. */ public static DataFileGroupInternal maybeSetIsolatedRoot( DataFileGroupInternal dataFileGroup, GroupKey groupKey) { // Check if isolated structure is allowed before adding the root if (!isIsolatedStructureAllowed(dataFileGroup)) { return dataFileGroup; } Hasher isolatedRootHasher = Hashing.sha256() .newHasher() .putUnencodedChars(dataFileGroup.getVariantId()) .putUnencodedChars(MddConstants.SPLIT_CHAR) .putUnencodedChars(groupKey.getAccount()) .putUnencodedChars(MddConstants.SPLIT_CHAR) .putLong(dataFileGroup.getBuildId()); String hash = isolatedRootHasher.hash().toString(); String directoryRoot = String.format("%s_%s", dataFileGroup.getGroupName(), hash); return dataFileGroup.toBuilder().setIsolatedDirectoryRoot(directoryRoot).build(); } /** Shared method to test whether the given file group supports isolated file structures. */ public static boolean isIsolatedStructureAllowed(DataFileGroupInternal dataFileGroupInternal) { if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP || !dataFileGroupInternal.getPreserveFilenamesAndIsolateFiles()) { return false; } // If any data file uses android blob sharing, don't create isolated file structure. for (DataFile dataFile : dataFileGroupInternal.getFileList()) { if (dataFile.getAndroidSharingType() == AndroidSharingType.ANDROID_BLOB_WHEN_AVAILABLE) { return false; } } return true; } /** * Gets the root directory where isolated files should be when the given file group supports * preserving relative file paths. */ public static Uri getIsolatedRootDirectory( Context context, Optional instanceId, DataFileGroupInternal fileGroupInternal) { String groupRoot; if (!fileGroupInternal.getIsolatedDirectoryRoot().isEmpty()) { groupRoot = fileGroupInternal.getIsolatedDirectoryRoot(); } else { // NOTE: Only the group name was used before the isolated directory root field was // added. To preserve backwards compatibility, fallback to group name if isolated directory // root is not present. groupRoot = fileGroupInternal.getGroupName(); } return DirectoryUtil.getDownloadSymlinkDirectory( context, fileGroupInternal.getAllowedReadersEnum(), instanceId) .buildUpon() .appendPath(groupRoot) .build(); } /** * Gets the isolated location of a given DataFile when the parent file group supports preserving * relative file paths. */ public static Uri getIsolatedFileUri( Context context, Optional instanceId, DataFile dataFile, DataFileGroupInternal parentFileGroup) { Uri rootUri = getIsolatedRootDirectory(context, instanceId, parentFileGroup); return appendIsolatedFileUri(rootUri, dataFile); } /** Helper method to append isolated file uri to an already known root. */ public static Uri appendIsolatedFileUri(Uri rootUri, DataFile dataFile) { Uri.Builder fileUriBuilder = rootUri.buildUpon(); if (dataFile.getRelativeFilePath().isEmpty()) { // If no relative path specified get the last segment from the // urlToDownload. String urlToDownload = dataFile.getUrlToDownload(); fileUriBuilder.appendPath(urlToDownload.substring(urlToDownload.lastIndexOf("/") + 1)); } else { // Use give relative path to get parts for (String part : dataFile.getRelativeFilePath().split("/", -1)) { if (!part.isEmpty()) { fileUriBuilder.appendPath(part); } } } return fileUriBuilder.build(); } /** * Removes the isolated file structure for the given file group. * *

If the isolated structure has already been deleted or was never created, this method is a * no-op. */ public static void removeIsolatedFileStructure( Context context, Optional instanceId, DataFileGroupInternal dataFileGroup, SynchronousFileStorage fileStorage) throws IOException { Uri isolatedRootDir = FileGroupUtil.getIsolatedRootDirectory(context, instanceId, dataFileGroup); if (fileStorage.exists(isolatedRootDir)) { Void unused = fileStorage.open(isolatedRootDir, RecursiveDeleteOpener.create().withNoFollowLinks()); } } public static boolean hasZipDownloadTransform(DataFile dataFile) { if (dataFile.hasDownloadTransforms()) { for (Transform transform : dataFile.getDownloadTransforms().getTransformList()) { if (transform.hasZip()) { return true; } } } return false; } public static boolean hasCompressDownloadTransform(DataFile dataFile) { if (dataFile.hasDownloadTransforms()) { for (Transform transform : dataFile.getDownloadTransforms().getTransformList()) { if (transform.hasCompress()) { return true; } } } return false; } public static String getFileChecksum(DataFile dataFile) { return hasZipDownloadTransform(dataFile) ? dataFile.getDownloadedFileChecksum() : dataFile.getChecksum(); } public static boolean isSideloadedFile(DataFile dataFile) { return isFileWithMatchingScheme( dataFile.getUrlToDownload(), ImmutableSet.of( MddConstants.SIDELOAD_FILE_URL_SCHEME, MddConstants.EMBEDDED_ASSET_URL_SCHEME)); } public static boolean isInlineFile(DataFile dataFile) { return isFileWithMatchingScheme( dataFile.getUrlToDownload(), ImmutableSet.of(MddConstants.INLINE_FILE_URL_SCHEME)); } public static boolean isInlineFile(String url) { return isFileWithMatchingScheme(url, ImmutableSet.of(MddConstants.INLINE_FILE_URL_SCHEME)); } // Helper method to test whether a DataFile's url scheme is contained in the given scheme set. private static boolean isFileWithMatchingScheme(String url, ImmutableSet schemes) { if (url.isEmpty()) { return false; } int colon = url.indexOf(':'); // TODO(b/196593240): Ensure this is always handled, or replace with a checked exception Preconditions.checkState(colon > -1, "Invalid url: %s", url); String fileScheme = url.substring(0, colon); for (String scheme : schemes) { if (Ascii.equalsIgnoreCase(fileScheme, scheme)) { return true; } } return false; } public static int getInlineFileCount(DataFileGroupInternal fileGroup) { int inlineFileCount = 0; for (DataFile file : fileGroup.getFileList()) { if (isInlineFile(file)) { inlineFileCount++; } } return inlineFileCount; } }