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.util; 17 18 import android.content.Context; 19 import android.net.Uri; 20 import android.os.Build.VERSION; 21 import android.os.Build.VERSION_CODES; 22 import com.google.android.libraries.mobiledatadownload.TimeSource; 23 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 24 import com.google.android.libraries.mobiledatadownload.file.openers.RecursiveDeleteOpener; 25 import com.google.android.libraries.mobiledatadownload.internal.MddConstants; 26 import com.google.common.base.Ascii; 27 import com.google.common.base.Optional; 28 import com.google.common.base.Preconditions; 29 import com.google.common.base.Strings; 30 import com.google.common.collect.ImmutableSet; 31 import com.google.common.hash.Hasher; 32 import com.google.common.hash.Hashing; 33 import com.google.mobiledatadownload.TransformProto.Transform; 34 import com.google.mobiledatadownload.internal.MetadataProto.DataFile; 35 import com.google.mobiledatadownload.internal.MetadataProto.DataFile.AndroidSharingType; 36 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupBookkeeping; 37 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal; 38 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; 39 import java.io.IOException; 40 import java.util.concurrent.TimeUnit; 41 import javax.annotation.Nullable; 42 43 /** A collection of util methods for interaction with a DataFileGroup proto. */ 44 public class FileGroupUtil { 45 46 /** 47 * @return the expiration date of this active file group in millis or Long.MAX_VALUE if no 48 * expiration date is set. 49 */ getExpirationDateMillis(DataFileGroupInternal fileGroup)50 public static long getExpirationDateMillis(DataFileGroupInternal fileGroup) { 51 return (fileGroup.getExpirationDateSecs() == 0) 52 ? Long.MAX_VALUE 53 : TimeUnit.SECONDS.toMillis(fileGroup.getExpirationDateSecs()); 54 } 55 56 /** Returns the expiration date of this stale file group in millis. */ getStaleExpirationDateMillis(DataFileGroupInternal fileGroup)57 public static long getStaleExpirationDateMillis(DataFileGroupInternal fileGroup) { 58 return TimeUnit.SECONDS.toMillis(fileGroup.getBookkeeping().getStaleExpirationDate()); 59 } 60 61 /** 62 * @return if the active group's expiration date has passed. False if no expiration date is set. 63 */ isActiveGroupExpired( DataFileGroupInternal fileGroup, TimeSource timeSource)64 public static boolean isActiveGroupExpired( 65 DataFileGroupInternal fileGroup, TimeSource timeSource) { 66 return isExpired(getExpirationDateMillis(fileGroup), timeSource); 67 } 68 69 /** 70 * @param expirationDateMillis the date (in millis since epoch) at which expiration should occur. 71 * @return if expirationDate has passed. 72 */ isExpired(long expirationDateMillis, TimeSource timeSource)73 public static boolean isExpired(long expirationDateMillis, TimeSource timeSource) { 74 return expirationDateMillis <= timeSource.currentTimeMillis(); 75 } 76 77 /** 78 * Returns file group key which uniquely identify a file group. 79 * 80 * @param groupName The file group name 81 * @param ownerPackage File Group owner package. For legacy reasons, this might not be set in some 82 * groups. 83 */ createGroupKey(String groupName, @Nullable String ownerPackage)84 public static GroupKey createGroupKey(String groupName, @Nullable String ownerPackage) { 85 GroupKey.Builder groupKey = GroupKey.newBuilder().setGroupName(groupName); 86 87 if (Strings.isNullOrEmpty(ownerPackage)) { 88 groupKey.setOwnerPackage(MddConstants.GMS_PACKAGE); 89 } else { 90 groupKey.setOwnerPackage(ownerPackage); 91 } 92 93 return groupKey.build(); 94 } 95 96 /** 97 * Returns the DataFile within dataFileGroup with the matching fileId. null if the group no such 98 * DataFile exists. 99 */ 100 @Nullable getFileFromGroupWithId( @ullable DataFileGroupInternal dataFileGroup, String fileId)101 public static DataFile getFileFromGroupWithId( 102 @Nullable DataFileGroupInternal dataFileGroup, String fileId) { 103 if (dataFileGroup == null) { 104 return null; 105 } 106 for (DataFile dataFile : dataFileGroup.getFileList()) { 107 if (fileId.equals(dataFile.getFileId())) { 108 return dataFile; 109 } 110 } 111 return null; 112 } 113 setStaleExpirationDate( DataFileGroupInternal dataFileGroup, long timeSinceEpoch)114 public static DataFileGroupInternal setStaleExpirationDate( 115 DataFileGroupInternal dataFileGroup, long timeSinceEpoch) { 116 DataFileGroupBookkeeping bookkeeping = 117 dataFileGroup.getBookkeeping().toBuilder().setStaleExpirationDate(timeSinceEpoch).build(); 118 dataFileGroup = dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build(); 119 return dataFileGroup; 120 } 121 setGroupNewFilesReceivedTimestamp( DataFileGroupInternal dataFileGroup, long timeSinceEpoch)122 public static DataFileGroupInternal setGroupNewFilesReceivedTimestamp( 123 DataFileGroupInternal dataFileGroup, long timeSinceEpoch) { 124 DataFileGroupBookkeeping bookkeeping = 125 dataFileGroup.getBookkeeping().toBuilder() 126 .setGroupNewFilesReceivedTimestamp(timeSinceEpoch) 127 .build(); 128 dataFileGroup = dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build(); 129 return dataFileGroup; 130 } 131 132 /** Sets the given downloaded timestamp in the given group */ setDownloadedTimestampInMillis( DataFileGroupInternal dataFileGroup, long timeSinceEpochInMillis)133 public static DataFileGroupInternal setDownloadedTimestampInMillis( 134 DataFileGroupInternal dataFileGroup, long timeSinceEpochInMillis) { 135 DataFileGroupBookkeeping bookkeeping = 136 dataFileGroup.getBookkeeping().toBuilder() 137 .setGroupDownloadedTimestampInMillis(timeSinceEpochInMillis) 138 .build(); 139 dataFileGroup = dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build(); 140 return dataFileGroup; 141 } 142 143 /** Sets the given download started timestamp in the given group. */ setDownloadStartedTimestampInMillis( DataFileGroupInternal dataFileGroup, long timeSinceEpochInMillis)144 public static DataFileGroupInternal setDownloadStartedTimestampInMillis( 145 DataFileGroupInternal dataFileGroup, long timeSinceEpochInMillis) { 146 DataFileGroupBookkeeping bookkeeping = 147 dataFileGroup.getBookkeeping().toBuilder() 148 .setGroupDownloadStartedTimestampInMillis(timeSinceEpochInMillis) 149 .build(); 150 dataFileGroup = dataFileGroup.toBuilder().setBookkeeping(bookkeeping).build(); 151 return dataFileGroup; 152 } 153 154 /** Sets the isolated root if the file group supports isolated structures. */ maybeSetIsolatedRoot( DataFileGroupInternal dataFileGroup, GroupKey groupKey)155 public static DataFileGroupInternal maybeSetIsolatedRoot( 156 DataFileGroupInternal dataFileGroup, GroupKey groupKey) { 157 // Check if isolated structure is allowed before adding the root 158 if (!isIsolatedStructureAllowed(dataFileGroup)) { 159 return dataFileGroup; 160 } 161 162 Hasher isolatedRootHasher = 163 Hashing.sha256() 164 .newHasher() 165 .putUnencodedChars(dataFileGroup.getVariantId()) 166 .putUnencodedChars(MddConstants.SPLIT_CHAR) 167 .putUnencodedChars(groupKey.getAccount()) 168 .putUnencodedChars(MddConstants.SPLIT_CHAR) 169 .putLong(dataFileGroup.getBuildId()); 170 171 String hash = isolatedRootHasher.hash().toString(); 172 String directoryRoot = String.format("%s_%s", dataFileGroup.getGroupName(), hash); 173 174 return dataFileGroup.toBuilder().setIsolatedDirectoryRoot(directoryRoot).build(); 175 } 176 177 /** Shared method to test whether the given file group supports isolated file structures. */ isIsolatedStructureAllowed(DataFileGroupInternal dataFileGroupInternal)178 public static boolean isIsolatedStructureAllowed(DataFileGroupInternal dataFileGroupInternal) { 179 if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP 180 || !dataFileGroupInternal.getPreserveFilenamesAndIsolateFiles()) { 181 return false; 182 } 183 184 // If any data file uses android blob sharing, don't create isolated file structure. 185 for (DataFile dataFile : dataFileGroupInternal.getFileList()) { 186 if (dataFile.getAndroidSharingType() == AndroidSharingType.ANDROID_BLOB_WHEN_AVAILABLE) { 187 return false; 188 } 189 } 190 191 return true; 192 } 193 194 /** 195 * Gets the root directory where isolated files should be when the given file group supports 196 * preserving relative file paths. 197 */ getIsolatedRootDirectory( Context context, Optional<String> instanceId, DataFileGroupInternal fileGroupInternal)198 public static Uri getIsolatedRootDirectory( 199 Context context, Optional<String> instanceId, DataFileGroupInternal fileGroupInternal) { 200 String groupRoot; 201 if (!fileGroupInternal.getIsolatedDirectoryRoot().isEmpty()) { 202 groupRoot = fileGroupInternal.getIsolatedDirectoryRoot(); 203 } else { 204 // NOTE: Only the group name was used before the isolated directory root field was 205 // added. To preserve backwards compatibility, fallback to group name if isolated directory 206 // root is not present. 207 groupRoot = fileGroupInternal.getGroupName(); 208 } 209 210 return DirectoryUtil.getDownloadSymlinkDirectory( 211 context, fileGroupInternal.getAllowedReadersEnum(), instanceId) 212 .buildUpon() 213 .appendPath(groupRoot) 214 .build(); 215 } 216 217 /** 218 * Gets the isolated location of a given DataFile when the parent file group supports preserving 219 * relative file paths. 220 */ getIsolatedFileUri( Context context, Optional<String> instanceId, DataFile dataFile, DataFileGroupInternal parentFileGroup)221 public static Uri getIsolatedFileUri( 222 Context context, 223 Optional<String> instanceId, 224 DataFile dataFile, 225 DataFileGroupInternal parentFileGroup) { 226 Uri rootUri = getIsolatedRootDirectory(context, instanceId, parentFileGroup); 227 return appendIsolatedFileUri(rootUri, dataFile); 228 } 229 230 /** Helper method to append isolated file uri to an already known root. */ appendIsolatedFileUri(Uri rootUri, DataFile dataFile)231 public static Uri appendIsolatedFileUri(Uri rootUri, DataFile dataFile) { 232 Uri.Builder fileUriBuilder = rootUri.buildUpon(); 233 if (dataFile.getRelativeFilePath().isEmpty()) { 234 // If no relative path specified get the last segment from the 235 // urlToDownload. 236 String urlToDownload = dataFile.getUrlToDownload(); 237 fileUriBuilder.appendPath(urlToDownload.substring(urlToDownload.lastIndexOf("/") + 1)); 238 } else { 239 // Use give relative path to get parts 240 for (String part : dataFile.getRelativeFilePath().split("/", -1)) { 241 if (!part.isEmpty()) { 242 fileUriBuilder.appendPath(part); 243 } 244 } 245 } 246 return fileUriBuilder.build(); 247 } 248 249 /** 250 * Removes the isolated file structure for the given file group. 251 * 252 * <p>If the isolated structure has already been deleted or was never created, this method is a 253 * no-op. 254 */ removeIsolatedFileStructure( Context context, Optional<String> instanceId, DataFileGroupInternal dataFileGroup, SynchronousFileStorage fileStorage)255 public static void removeIsolatedFileStructure( 256 Context context, 257 Optional<String> instanceId, 258 DataFileGroupInternal dataFileGroup, 259 SynchronousFileStorage fileStorage) 260 throws IOException { 261 Uri isolatedRootDir = 262 FileGroupUtil.getIsolatedRootDirectory(context, instanceId, dataFileGroup); 263 if (fileStorage.exists(isolatedRootDir)) { 264 Void unused = 265 fileStorage.open(isolatedRootDir, RecursiveDeleteOpener.create().withNoFollowLinks()); 266 } 267 } 268 hasZipDownloadTransform(DataFile dataFile)269 public static boolean hasZipDownloadTransform(DataFile dataFile) { 270 if (dataFile.hasDownloadTransforms()) { 271 for (Transform transform : dataFile.getDownloadTransforms().getTransformList()) { 272 if (transform.hasZip()) { 273 return true; 274 } 275 } 276 } 277 return false; 278 } 279 hasCompressDownloadTransform(DataFile dataFile)280 public static boolean hasCompressDownloadTransform(DataFile dataFile) { 281 if (dataFile.hasDownloadTransforms()) { 282 for (Transform transform : dataFile.getDownloadTransforms().getTransformList()) { 283 if (transform.hasCompress()) { 284 return true; 285 } 286 } 287 } 288 return false; 289 } 290 getFileChecksum(DataFile dataFile)291 public static String getFileChecksum(DataFile dataFile) { 292 return hasZipDownloadTransform(dataFile) 293 ? dataFile.getDownloadedFileChecksum() 294 : dataFile.getChecksum(); 295 } 296 isSideloadedFile(DataFile dataFile)297 public static boolean isSideloadedFile(DataFile dataFile) { 298 return isFileWithMatchingScheme( 299 dataFile.getUrlToDownload(), 300 ImmutableSet.of( 301 MddConstants.SIDELOAD_FILE_URL_SCHEME, MddConstants.EMBEDDED_ASSET_URL_SCHEME)); 302 } 303 isInlineFile(DataFile dataFile)304 public static boolean isInlineFile(DataFile dataFile) { 305 return isFileWithMatchingScheme( 306 dataFile.getUrlToDownload(), ImmutableSet.of(MddConstants.INLINE_FILE_URL_SCHEME)); 307 } 308 isInlineFile(String url)309 public static boolean isInlineFile(String url) { 310 return isFileWithMatchingScheme(url, ImmutableSet.of(MddConstants.INLINE_FILE_URL_SCHEME)); 311 } 312 313 // Helper method to test whether a DataFile's url scheme is contained in the given scheme set. isFileWithMatchingScheme(String url, ImmutableSet<String> schemes)314 private static boolean isFileWithMatchingScheme(String url, ImmutableSet<String> schemes) { 315 if (url.isEmpty()) { 316 return false; 317 } 318 int colon = url.indexOf(':'); 319 // TODO(b/196593240): Ensure this is always handled, or replace with a checked exception 320 Preconditions.checkState(colon > -1, "Invalid url: %s", url); 321 String fileScheme = url.substring(0, colon); 322 for (String scheme : schemes) { 323 if (Ascii.equalsIgnoreCase(fileScheme, scheme)) { 324 return true; 325 } 326 } 327 return false; 328 } 329 getInlineFileCount(DataFileGroupInternal fileGroup)330 public static int getInlineFileCount(DataFileGroupInternal fileGroup) { 331 int inlineFileCount = 0; 332 for (DataFile file : fileGroup.getFileList()) { 333 if (isInlineFile(file)) { 334 inlineFileCount++; 335 } 336 } 337 338 return inlineFileCount; 339 } 340 } 341