• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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