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.populator; 17 18 import android.accounts.Account; 19 import com.google.android.libraries.mobiledatadownload.AddFileGroupRequest; 20 import com.google.android.libraries.mobiledatadownload.AggregateException; 21 import com.google.android.libraries.mobiledatadownload.MobileDataDownload; 22 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 23 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture; 24 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 25 import com.google.common.base.Optional; 26 import com.google.common.util.concurrent.FutureCallback; 27 import com.google.common.util.concurrent.Futures; 28 import com.google.common.util.concurrent.ListenableFuture; 29 import com.google.common.util.concurrent.MoreExecutors; 30 import com.google.mobiledatadownload.DownloadConfigProto.DataFile; 31 import com.google.mobiledatadownload.DownloadConfigProto.DataFileGroup; 32 import com.google.mobiledatadownload.DownloadConfigProto.DeltaFile; 33 import com.google.mobiledatadownload.DownloadConfigProto.ManifestConfig; 34 import java.util.ArrayList; 35 import java.util.List; 36 37 /** Shared functions for ManifestConfig. */ 38 public final class ManifestConfigHelper { 39 public static final String URL_TEMPLATE_CHECKSUM_PLACEHOLDER = "{checksum}"; 40 41 private static final String TAG = "ManifestConfigHelper"; 42 43 private final MobileDataDownload mobileDataDownload; 44 private final Optional<ManifestConfigOverrider> overriderOptional; 45 private final List<Account> accounts; 46 private final boolean addGroupsWithVariantId; 47 48 /** Creates a new helper for converting manifest configs into data file groups. */ ManifestConfigHelper( MobileDataDownload mobileDataDownload, Optional<ManifestConfigOverrider> overriderOptional, List<Account> accounts, boolean addGroupsWithVariantId)49 ManifestConfigHelper( 50 MobileDataDownload mobileDataDownload, 51 Optional<ManifestConfigOverrider> overriderOptional, 52 List<Account> accounts, 53 boolean addGroupsWithVariantId) { 54 this.mobileDataDownload = mobileDataDownload; 55 this.overriderOptional = overriderOptional; 56 this.accounts = accounts; 57 this.addGroupsWithVariantId = addGroupsWithVariantId; 58 } 59 60 /** 61 * Reads file groups from {@link ManifestConfig} and adds to MDD after applying the {@link 62 * ManifestConfigOverrider} if it's present. 63 * 64 * <p>This static method encapsulates shared logic between a few populators: 65 * 66 * <ul> 67 * <li>{@link ManifestFileGroupPopulator} 68 * <li>{@link ManifestConfigFlagPopulator} 69 * <li>{@link LocalManifestFileGroupPopulator} 70 * <li>{@link EmbeddedAssetManifestPopulator} 71 * </ul> 72 * 73 * @param mobileDataDownload The MDD instance 74 * @param manifestConfig The proto that contains configs for file groups and modifiers 75 * @param overriderOptional An optional overrider that takes manifest config and returns a list of 76 * file groups to be added ot MDD 77 * @param accounts A list of accounts that the parsed file groups should be associated with 78 * @param addGroupsWithVariantId whether variantId should be included when adding the parsed file 79 * groups 80 */ refreshFromManifestConfig( MobileDataDownload mobileDataDownload, ManifestConfig manifestConfig, Optional<ManifestConfigOverrider> overriderOptional, List<Account> accounts, boolean addGroupsWithVariantId)81 static ListenableFuture<Void> refreshFromManifestConfig( 82 MobileDataDownload mobileDataDownload, 83 ManifestConfig manifestConfig, 84 Optional<ManifestConfigOverrider> overriderOptional, 85 List<Account> accounts, 86 boolean addGroupsWithVariantId) { 87 ManifestConfigHelper helper = 88 new ManifestConfigHelper( 89 mobileDataDownload, overriderOptional, accounts, addGroupsWithVariantId); 90 return PropagatedFluentFuture.from(helper.applyOverrider(manifestConfig)) 91 .transformAsync(helper::addAllFileGroups, MoreExecutors.directExecutor()) 92 .catchingAsync( 93 AggregateException.class, 94 ex -> Futures.immediateVoidFuture(), 95 MoreExecutors.directExecutor()); 96 } 97 98 /** Adds the specified list of file groups to MDD. */ addAllFileGroups(List<DataFileGroup> fileGroups)99 ListenableFuture<Void> addAllFileGroups(List<DataFileGroup> fileGroups) { 100 List<ListenableFuture<Boolean>> addFileGroupFutures = new ArrayList<>(); 101 Optional<String> variantId = Optional.absent(); 102 103 for (DataFileGroup dataFileGroup : fileGroups) { 104 if (dataFileGroup == null || dataFileGroup.getGroupName().isEmpty()) { 105 continue; 106 } 107 108 // Include variantId if variant is present and helper is configured to do so 109 if (addGroupsWithVariantId && !dataFileGroup.getVariantId().isEmpty()) { 110 variantId = Optional.of(dataFileGroup.getVariantId()); 111 } 112 113 AddFileGroupRequest.Builder addFileGroupRequestBuilder = 114 AddFileGroupRequest.newBuilder() 115 .setDataFileGroup(dataFileGroup) 116 .setVariantIdOptional(variantId); 117 118 // Add once without any account 119 ListenableFuture<Boolean> addFileGroupFuture = 120 mobileDataDownload.addFileGroup(addFileGroupRequestBuilder.build()); 121 attachLoggingCallback( 122 addFileGroupFuture, 123 dataFileGroup.getGroupName(), 124 /* account= */ Optional.absent(), 125 variantId); 126 addFileGroupFutures.add(addFileGroupFuture); 127 128 // Add for each account 129 for (Account account : accounts) { 130 ListenableFuture<Boolean> addFileGroupFutureWithAccount = 131 mobileDataDownload.addFileGroup( 132 addFileGroupRequestBuilder.setAccountOptional(Optional.of(account)).build()); 133 attachLoggingCallback( 134 addFileGroupFutureWithAccount, 135 dataFileGroup.getGroupName(), 136 Optional.of(account), 137 variantId); 138 addFileGroupFutures.add(addFileGroupFutureWithAccount); 139 } 140 } 141 return PropagatedFutures.whenAllComplete(addFileGroupFutures) 142 .call( 143 () -> { 144 AggregateException.throwIfFailed(addFileGroupFutures, "Failed to add file groups"); 145 return null; 146 }, 147 MoreExecutors.directExecutor()); 148 } 149 attachLoggingCallback( ListenableFuture<Boolean> addFileGroupFuture, String groupName, Optional<Account> account, Optional<String> variant)150 private void attachLoggingCallback( 151 ListenableFuture<Boolean> addFileGroupFuture, 152 String groupName, 153 Optional<Account> account, 154 Optional<String> variant) { 155 PropagatedFutures.addCallback( 156 addFileGroupFuture, 157 new FutureCallback<Boolean>() { 158 @Override 159 public void onSuccess(Boolean result) { 160 if (result.booleanValue()) { 161 LogUtil.d( 162 "%s: Added file group %s with account: %s, variant: %s", 163 TAG, 164 groupName, 165 String.valueOf(account.orNull()), 166 String.valueOf(variant.orNull())); 167 } else { 168 LogUtil.d( 169 "%s: Failed to add file group %s with account: %s, variant: %s", 170 TAG, 171 groupName, 172 String.valueOf(account.orNull()), 173 String.valueOf(variant.orNull())); 174 } 175 } 176 177 @Override 178 public void onFailure(Throwable t) { 179 LogUtil.e( 180 t, 181 "%s: Failed to add file group %s with account: %s, variant: %s", 182 TAG, 183 groupName, 184 String.valueOf(account.orNull()), 185 String.valueOf(variant.orNull())); 186 } 187 }, 188 MoreExecutors.directExecutor()); 189 } 190 191 /** Applies the overrider to the manifest config to generate a list of file groups for adding. */ applyOverrider(ManifestConfig manifestConfig)192 ListenableFuture<List<DataFileGroup>> applyOverrider(ManifestConfig manifestConfig) { 193 if (overriderOptional.isPresent()) { 194 return overriderOptional.get().override(maybeApplyFileUrlTemplate(manifestConfig)); 195 } 196 List<DataFileGroup> results = new ArrayList<>(); 197 for (ManifestConfig.Entry entry : maybeApplyFileUrlTemplate(manifestConfig).getEntryList()) { 198 results.add(entry.getDataFileGroup()); 199 } 200 return Futures.immediateFuture(results); 201 } 202 203 /** 204 * If file_url_template is populated and file url_to_download field is empty in the {@code 205 * ManifestConfig} manifestConfig then construct the url_to_download field using the template. 206 * 207 * <p>NOTE: If file_url_template is empty then the files are expected to have the complete 208 * download URL, validate and throw an {@link IllegalArgumentException} if url_to_download is not 209 * populated. 210 */ maybeApplyFileUrlTemplate(ManifestConfig manifestConfig)211 public static ManifestConfig maybeApplyFileUrlTemplate(ManifestConfig manifestConfig) { 212 if (!manifestConfig.hasUrlTemplate() 213 || manifestConfig.getUrlTemplate().getFileUrlTemplate().isEmpty()) { 214 return validateManifestConfigFileUrls(manifestConfig); 215 } 216 String fileDownloadUrlTemplate = manifestConfig.getUrlTemplate().getFileUrlTemplate(); 217 ManifestConfig.Builder updatedManifestConfigBuilder = manifestConfig.toBuilder().clearEntry(); 218 219 for (ManifestConfig.Entry entry : manifestConfig.getEntryList()) { 220 DataFileGroup.Builder dataFileGroupBuilder = entry.getDataFileGroup().toBuilder().clearFile(); 221 for (DataFile dataFile : entry.getDataFileGroup().getFileList()) { 222 DataFile.Builder dataFileBuilder = dataFile.toBuilder().clearDeltaFile(); 223 224 if (dataFile.getUrlToDownload().isEmpty()) { 225 dataFileBuilder.setUrlToDownload( 226 fileDownloadUrlTemplate.replace( 227 URL_TEMPLATE_CHECKSUM_PLACEHOLDER, dataFile.getChecksum())); 228 } 229 230 for (DeltaFile deltaFile : dataFile.getDeltaFileList()) { 231 dataFileBuilder.addDeltaFile( 232 deltaFile.getUrlToDownload().isEmpty() 233 ? deltaFile.toBuilder() 234 .setUrlToDownload( 235 fileDownloadUrlTemplate.replace( 236 URL_TEMPLATE_CHECKSUM_PLACEHOLDER, deltaFile.getChecksum())) 237 .build() 238 : deltaFile); 239 } 240 241 dataFileGroupBuilder.addFile(dataFileBuilder); 242 } 243 updatedManifestConfigBuilder.addEntry( 244 entry.toBuilder().setDataFileGroup(dataFileGroupBuilder)); 245 } 246 return updatedManifestConfigBuilder.build(); 247 } 248 249 /** 250 * Validates that all the files in {@code ManifestConfig} manifestConfig have the url_to_download 251 * populated. 252 */ validateManifestConfigFileUrls(ManifestConfig manifestConfig)253 private static ManifestConfig validateManifestConfigFileUrls(ManifestConfig manifestConfig) { 254 for (ManifestConfig.Entry entry : manifestConfig.getEntryList()) { 255 for (DataFile dataFile : entry.getDataFileGroup().getFileList()) { 256 if (dataFile.getUrlToDownload().isEmpty()) { 257 throw new IllegalArgumentException( 258 String.format("DataFile %s url_to_download is missing.", dataFile.getFileId())); 259 } 260 for (DeltaFile deltaFile : dataFile.getDeltaFileList()) { 261 if (deltaFile.getUrlToDownload().isEmpty()) { 262 throw new IllegalArgumentException( 263 String.format( 264 "DeltaFile for file %s url_to_download is missing.", dataFile.getFileId())); 265 } 266 } 267 } 268 } 269 return manifestConfig; 270 } 271 } 272