1 /* 2 * Copyright (C) 2022 The Android Open Source Project 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 17 package com.android.adservices.download; 18 19 import static com.android.adservices.service.topics.classifier.ModelManager.BUNDLED_CLASSIFIER_ASSETS_METADATA_FILE_PATH; 20 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.os.Build; 24 import android.os.SystemClock; 25 26 import androidx.annotation.RequiresApi; 27 28 import com.android.adservices.LogUtil; 29 import com.android.adservices.concurrency.AdServicesExecutors; 30 import com.android.adservices.service.Flags; 31 import com.android.adservices.service.FlagsFactory; 32 import com.android.adservices.service.consent.AdServicesApiType; 33 import com.android.adservices.service.consent.ConsentManager; 34 import com.android.adservices.service.topics.classifier.CommonClassifierHelper; 35 import com.android.adservices.service.ui.data.UxStatesManager; 36 import com.android.adservices.service.ui.ux.collection.PrivacySandboxUxCollection; 37 import com.android.adservices.shared.common.ApplicationContextSingleton; 38 import com.android.internal.annotations.VisibleForTesting; 39 40 import com.google.android.downloader.AndroidDownloaderLogger; 41 import com.google.android.downloader.ConnectivityHandler; 42 import com.google.android.downloader.DownloadConstraints; 43 import com.google.android.downloader.Downloader; 44 import com.google.android.downloader.PlatformUrlEngine; 45 import com.google.android.downloader.UrlEngine; 46 import com.google.android.libraries.mobiledatadownload.Logger; 47 import com.google.android.libraries.mobiledatadownload.MobileDataDownload; 48 import com.google.android.libraries.mobiledatadownload.MobileDataDownloadBuilder; 49 import com.google.android.libraries.mobiledatadownload.TimeSource; 50 import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; 51 import com.google.android.libraries.mobiledatadownload.downloader.offroad.ExceptionHandler; 52 import com.google.android.libraries.mobiledatadownload.downloader.offroad.Offroad2FileDownloader; 53 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 54 import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend; 55 import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend; 56 import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadMetadataStore; 57 import com.google.android.libraries.mobiledatadownload.file.integration.downloader.SharedPreferencesDownloadMetadata; 58 import com.google.android.libraries.mobiledatadownload.monitor.NetworkUsageMonitor; 59 import com.google.android.libraries.mobiledatadownload.populator.ManifestConfigFileParser; 60 import com.google.android.libraries.mobiledatadownload.populator.ManifestConfigOverrider; 61 import com.google.android.libraries.mobiledatadownload.populator.ManifestFileGroupPopulator; 62 import com.google.android.libraries.mobiledatadownload.populator.SharedPreferencesManifestFileMetadata; 63 import com.google.common.base.Optional; 64 import com.google.common.collect.ImmutableList; 65 import com.google.common.util.concurrent.Futures; 66 import com.google.common.util.concurrent.ListenableFuture; 67 import com.google.common.util.concurrent.ListeningExecutorService; 68 import com.google.mobiledatadownload.DownloadConfigProto; 69 import com.google.mobiledatadownload.DownloadConfigProto.ManifestFileFlag; 70 import com.google.protobuf.MessageLite; 71 72 import java.io.PrintWriter; 73 import java.util.ArrayList; 74 import java.util.List; 75 import java.util.concurrent.Executor; 76 77 /** Mobile Data Download Factory. */ 78 // TODO(b/269798827): Enable for R. 79 @RequiresApi(Build.VERSION_CODES.S) 80 public class MobileDataDownloadFactory { 81 private static MobileDataDownload sSingletonMdd; 82 private static SynchronousFileStorage sSynchronousFileStorage; 83 84 private static final String MDD_METADATA_SHARED_PREFERENCES = "mdd_metadata_store"; 85 private static final String TOPICS_MANIFEST_ID = "TopicsManifestId"; 86 private static final String MEASUREMENT_MANIFEST_ID = "MeasurementManifestId"; 87 private static final String ENCRYPTION_KEYS_MANIFEST_ID = "EncryptionKeysManifestId"; 88 private static final String UI_OTA_STRINGS_MANIFEST_ID = "UiOtaStringsManifestId"; 89 private static final String UI_OTA_RESOURCES_MANIFEST_ID = "UiOtaResourcesManifestId"; 90 private static final String ENROLLMENT_PROTO_MANIFEST_ID = "EnrollmentProtoManifestId"; 91 92 private static final int MAX_ADB_LOGCAT_SIZE = 4000; 93 94 /** Returns a singleton of MobileDataDownload for the whole PPAPI app. */ getMdd(Flags flags)95 public static MobileDataDownload getMdd(Flags flags) { 96 synchronized (MobileDataDownloadFactory.class) { 97 if (sSingletonMdd == null) { 98 // TODO(b/236761740): This only adds the core MDD code. We still need other 99 // components: 100 // Add Logger 101 // Add Configurator. 102 103 Context context = ApplicationContextSingleton.get(); 104 SynchronousFileStorage fileStorage = getFileStorage(); 105 FileDownloader fileDownloader = getFileDownloader(flags, fileStorage); 106 NetworkUsageMonitor networkUsageMonitor = 107 new NetworkUsageMonitor( 108 context, 109 new TimeSource() { 110 @Override 111 public long currentTimeMillis() { 112 return System.currentTimeMillis(); 113 } 114 115 @Override 116 public long elapsedRealtimeNanos() { 117 return SystemClock.elapsedRealtimeNanos(); 118 } 119 }); 120 121 MobileDataDownloadBuilder mobileDataDownloadBuilder = 122 MobileDataDownloadBuilder.newBuilder() 123 .setContext(context) 124 .setControlExecutor(getControlExecutor()) 125 .setNetworkUsageMonitor(networkUsageMonitor) 126 .setFileStorage(fileStorage) 127 .setFileDownloaderSupplier(() -> fileDownloader) 128 .addFileGroupPopulator( 129 getTopicsManifestPopulator( 130 flags, fileStorage, fileDownloader)) 131 .addFileGroupPopulator( 132 getMeasurementManifestPopulator( 133 flags, 134 fileStorage, 135 fileDownloader, 136 /* getProto= */ false)) 137 .addFileGroupPopulator( 138 getEncryptionKeysManifestPopulator( 139 context, flags, fileStorage, fileDownloader)) 140 .addFileGroupPopulator( 141 getUiOtaResourcesManifestPopulator( 142 flags, fileStorage, fileDownloader)) 143 .setLoggerOptional(getMddLogger(flags)) 144 .setFlagsOptional(Optional.of(MddFlags.getInstance())); 145 146 if (flags.getEnrollmentProtoFileEnabled()) { 147 mobileDataDownloadBuilder.addFileGroupPopulator( 148 getMeasurementManifestPopulator( 149 flags, fileStorage, fileDownloader, /* getProto= */ true)); 150 } 151 152 sSingletonMdd = mobileDataDownloadBuilder.build(); 153 } 154 155 return sSingletonMdd; 156 } 157 } 158 159 // Connectivity constraints will be checked by JobScheduler/WorkManager instead. 160 private static class NoOpConnectivityHandler implements ConnectivityHandler { 161 @Override checkConnectivity(DownloadConstraints constraints)162 public ListenableFuture<Void> checkConnectivity(DownloadConstraints constraints) { 163 return Futures.immediateVoidFuture(); 164 } 165 } 166 167 /** Return a singleton of {@link SynchronousFileStorage}. */ getFileStorage()168 public static SynchronousFileStorage getFileStorage() { 169 synchronized (MobileDataDownloadFactory.class) { 170 if (sSynchronousFileStorage == null) { 171 Context context = ApplicationContextSingleton.get(); 172 sSynchronousFileStorage = 173 new SynchronousFileStorage( 174 ImmutableList.of( 175 /*backends*/ AndroidFileBackend.builder(context).build(), 176 new JavaFileBackend()), 177 ImmutableList.of(/*transforms*/ ), 178 ImmutableList.of(/*monitors*/ )); 179 } 180 return sSynchronousFileStorage; 181 } 182 } 183 184 @VisibleForTesting getControlExecutor()185 static ListeningExecutorService getControlExecutor() { 186 return AdServicesExecutors.getBackgroundExecutor(); 187 } 188 getDownloadExecutor()189 private static Executor getDownloadExecutor() { 190 return AdServicesExecutors.getBackgroundExecutor(); 191 } 192 getUrlEngine(Flags flags)193 private static UrlEngine getUrlEngine(Flags flags) { 194 // TODO(b/219594618): Switch to use CronetUrlEngine. 195 return new PlatformUrlEngine( 196 AdServicesExecutors.getBlockingExecutor(), 197 /* connectTimeoutMs= */ flags.getDownloaderConnectionTimeoutMs(), 198 /* readTimeoutMs= */ flags.getDownloaderReadTimeoutMs()); 199 } 200 getExceptionHandler()201 private static ExceptionHandler getExceptionHandler() { 202 return ExceptionHandler.withDefaultHandling(); 203 } 204 205 @VisibleForTesting getFileDownloader(Flags flags, SynchronousFileStorage fileStorage)206 static FileDownloader getFileDownloader(Flags flags, SynchronousFileStorage fileStorage) { 207 DownloadMetadataStore downloadMetadataStore = getDownloadMetadataStore(); 208 209 Downloader downloader = 210 new Downloader.Builder() 211 .withIOExecutor(AdServicesExecutors.getBlockingExecutor()) 212 .withConnectivityHandler(new NoOpConnectivityHandler()) 213 .withMaxConcurrentDownloads(flags.getDownloaderMaxDownloadThreads()) 214 .withLogger(new AndroidDownloaderLogger()) 215 .addUrlEngine("https", getUrlEngine(flags)) 216 .build(); 217 218 return new Offroad2FileDownloader( 219 downloader, 220 fileStorage, 221 getDownloadExecutor(), 222 /* authTokenProvider */ null, 223 downloadMetadataStore, 224 getExceptionHandler(), 225 Optional.absent()); 226 } 227 getDownloadMetadataStore()228 private static DownloadMetadataStore getDownloadMetadataStore() { 229 Context context = ApplicationContextSingleton.get(); 230 SharedPreferences sharedPrefs = 231 context.getSharedPreferences(MDD_METADATA_SHARED_PREFERENCES, Context.MODE_PRIVATE); 232 DownloadMetadataStore downloadMetadataStore = 233 new SharedPreferencesDownloadMetadata( 234 sharedPrefs, AdServicesExecutors.getBackgroundExecutor()); 235 return downloadMetadataStore; 236 } 237 238 // Create the Manifest File Group Populator for Topics Classifier. 239 @VisibleForTesting getTopicsManifestPopulator( Flags flags, SynchronousFileStorage fileStorage, FileDownloader fileDownloader)240 static ManifestFileGroupPopulator getTopicsManifestPopulator( 241 Flags flags, SynchronousFileStorage fileStorage, FileDownloader fileDownloader) { 242 Context context = ApplicationContextSingleton.get(); 243 244 ManifestFileFlag manifestFileFlag = 245 ManifestFileFlag.newBuilder() 246 .setManifestId(TOPICS_MANIFEST_ID) 247 .setManifestFileUrl(flags.getMddTopicsClassifierManifestFileUrl()) 248 .build(); 249 250 ManifestConfigFileParser manifestConfigFileParser = 251 new ManifestConfigFileParser( 252 fileStorage, AdServicesExecutors.getBackgroundExecutor()); 253 254 // Only download Topics classifier model when Mdd model build_id is greater than bundled 255 // model. 256 ManifestConfigOverrider manifestConfigOverrider = 257 manifestConfig -> { 258 List<DownloadConfigProto.DataFileGroup> groups = new ArrayList<>(); 259 for (DownloadConfigProto.ManifestConfig.Entry entry : 260 manifestConfig.getEntryList()) { 261 long dataFileGroupBuildId = entry.getDataFileGroup().getBuildId(); 262 long bundledModelBuildId = 263 CommonClassifierHelper.getBundledModelBuildId( 264 context, BUNDLED_CLASSIFIER_ASSETS_METADATA_FILE_PATH); 265 if (dataFileGroupBuildId > bundledModelBuildId) { 266 groups.add(entry.getDataFileGroup()); 267 LogUtil.d("Added topics classifier file group to MDD"); 268 } else { 269 LogUtil.d( 270 "Topics Classifier's Bundled BuildId = %d is bigger than or" 271 + " equal to the BuildId = %d from Server side, skipping" 272 + " the downloading.", 273 bundledModelBuildId, dataFileGroupBuildId); 274 } 275 } 276 return Futures.immediateFuture(groups); 277 }; 278 279 return ManifestFileGroupPopulator.builder() 280 .setContext(context) 281 .setEnabledSupplier( 282 () -> { 283 // Topics is permanently disabled for U18 UX. 284 if (UxStatesManager.getInstance().getUx() 285 == PrivacySandboxUxCollection.U18_UX) { 286 return false; 287 } 288 289 // Topics resources should not be downloaded pre-consent. 290 if (flags.getGaUxFeatureEnabled()) { 291 return ConsentManager.getInstance() 292 .getConsent(AdServicesApiType.TOPICS) 293 .isGiven(); 294 } else { 295 return ConsentManager.getInstance().getConsent().isGiven(); 296 } 297 }) 298 .setBackgroundExecutor(AdServicesExecutors.getBackgroundExecutor()) 299 .setFileDownloader(() -> fileDownloader) 300 .setFileStorage(fileStorage) 301 .setManifestFileFlagSupplier(() -> manifestFileFlag) 302 .setManifestConfigParser(manifestConfigFileParser) 303 .setMetadataStore( 304 SharedPreferencesManifestFileMetadata.createFromContext( 305 context, /*InstanceId*/ 306 Optional.absent(), 307 AdServicesExecutors.getBackgroundExecutor())) 308 // TODO(b/239265537): Enable Dedup using Etag. 309 .setDedupDownloadWithEtag(false) 310 .setOverriderOptional(Optional.of(manifestConfigOverrider)) 311 // TODO(b/243829623): use proper Logger. 312 .setLogger( 313 new Logger() { 314 @Override 315 public void log(MessageLite event, int eventCode) { 316 // A no-op logger. 317 } 318 }) 319 .build(); 320 } 321 322 @VisibleForTesting 323 static ManifestFileGroupPopulator getUiOtaResourcesManifestPopulator( 324 Flags flags, SynchronousFileStorage fileStorage, FileDownloader fileDownloader) { 325 Context context = ApplicationContextSingleton.get(); 326 327 ManifestFileFlag manifestFileFlag = 328 ManifestFileFlag.newBuilder() 329 .setManifestId( 330 flags.getUiOtaResourcesFeatureEnabled() 331 ? UI_OTA_RESOURCES_MANIFEST_ID 332 : UI_OTA_STRINGS_MANIFEST_ID) 333 .setManifestFileUrl( 334 flags.getUiOtaResourcesFeatureEnabled() 335 ? flags.getUiOtaResourcesManifestFileUrl() 336 : flags.getUiOtaStringsManifestFileUrl()) 337 .build(); 338 339 ManifestConfigFileParser manifestConfigFileParser = 340 new ManifestConfigFileParser( 341 fileStorage, AdServicesExecutors.getBackgroundExecutor()); 342 343 return ManifestFileGroupPopulator.builder() 344 .setContext(context) 345 // OTA resources can be downloaded pre-consent before notification, or with consent 346 .setEnabledSupplier( 347 () -> 348 !ConsentManager.getInstance().wasGaUxNotificationDisplayed() 349 || isAnyConsentGiven(flags)) 350 .setBackgroundExecutor(AdServicesExecutors.getBackgroundExecutor()) 351 .setFileDownloader(() -> fileDownloader) 352 .setFileStorage(fileStorage) 353 .setManifestFileFlagSupplier(() -> manifestFileFlag) 354 .setManifestConfigParser(manifestConfigFileParser) 355 .setMetadataStore( 356 SharedPreferencesManifestFileMetadata.createFromContext( 357 context, /*InstanceId*/ 358 Optional.absent(), 359 AdServicesExecutors.getBackgroundExecutor())) 360 // TODO(b/239265537): Enable dedup using etag. 361 .setDedupDownloadWithEtag(false) 362 // TODO(b/236761740): user proper Logger. 363 .setLogger( 364 new Logger() { 365 @Override 366 public void log(MessageLite event, int eventCode) { 367 // A no-op logger. 368 } 369 }) 370 .build(); 371 } 372 373 private static boolean isAnyConsentGiven(Flags flags) { 374 ConsentManager instance = ConsentManager.getInstance(); 375 if (flags.getGaUxFeatureEnabled() 376 && (instance.getConsent(AdServicesApiType.MEASUREMENTS).isGiven() 377 || instance.getConsent(AdServicesApiType.TOPICS).isGiven() 378 || instance.getConsent(AdServicesApiType.FLEDGE).isGiven())) { 379 return true; 380 } 381 382 return instance.getConsent().isGiven(); 383 } 384 385 @VisibleForTesting 386 static ManifestFileGroupPopulator getMeasurementManifestPopulator( 387 Flags flags, 388 SynchronousFileStorage fileStorage, 389 FileDownloader fileDownloader, 390 boolean getProto) { 391 Context context = ApplicationContextSingleton.get(); 392 393 String manifestId = getProto ? ENROLLMENT_PROTO_MANIFEST_ID : MEASUREMENT_MANIFEST_ID; 394 String fileUrl = 395 getProto 396 ? flags.getMddEnrollmentManifestFileUrl() 397 : flags.getMeasurementManifestFileUrl(); 398 399 ManifestFileFlag manifestFileFlag = 400 ManifestFileFlag.newBuilder() 401 .setManifestId(manifestId) 402 .setManifestFileUrl(fileUrl) 403 .build(); 404 405 ManifestConfigFileParser manifestConfigFileParser = 406 new ManifestConfigFileParser( 407 fileStorage, AdServicesExecutors.getBackgroundExecutor()); 408 409 return ManifestFileGroupPopulator.builder() 410 .setContext(context) 411 // measurement resources should not be downloaded pre-consent 412 .setEnabledSupplier( 413 () -> { 414 if (flags.getGaUxFeatureEnabled()) { 415 return isAnyConsentGiven(flags); 416 } else { 417 return ConsentManager.getInstance().getConsent().isGiven(); 418 } 419 }) 420 .setBackgroundExecutor(AdServicesExecutors.getBackgroundExecutor()) 421 .setFileDownloader(() -> fileDownloader) 422 .setFileStorage(fileStorage) 423 .setManifestFileFlagSupplier(() -> manifestFileFlag) 424 .setManifestConfigParser(manifestConfigFileParser) 425 .setMetadataStore( 426 SharedPreferencesManifestFileMetadata.createFromContext( 427 context, /*InstanceId*/ 428 Optional.absent(), 429 AdServicesExecutors.getBackgroundExecutor())) 430 // TODO(b/239265537): Enable dedup using etag. 431 .setDedupDownloadWithEtag(false) 432 // TODO(b/243829623): use proper Logger. 433 .setLogger( 434 new Logger() { 435 @Override 436 public void log(MessageLite event, int eventCode) { 437 // A no-op logger. 438 } 439 }) 440 .build(); 441 } 442 443 @VisibleForTesting 444 static ManifestFileGroupPopulator getEncryptionKeysManifestPopulator( 445 Context context, 446 Flags flags, 447 SynchronousFileStorage fileStorage, 448 FileDownloader fileDownloader) { 449 ManifestFileFlag manifestFileFlag = 450 ManifestFileFlag.newBuilder() 451 .setManifestId(ENCRYPTION_KEYS_MANIFEST_ID) 452 .setManifestFileUrl(flags.getMddEncryptionKeysManifestFileUrl()) 453 .build(); 454 455 ManifestConfigFileParser manifestConfigFileParser = 456 new ManifestConfigFileParser( 457 fileStorage, AdServicesExecutors.getBackgroundExecutor()); 458 459 return ManifestFileGroupPopulator.builder() 460 .setContext(context) 461 // encryption key resources should not be downloaded pre-consent 462 .setEnabledSupplier( 463 () -> { 464 if (flags.getGaUxFeatureEnabled()) { 465 return isAnyConsentGiven(flags); 466 } else { 467 return ConsentManager.getInstance().getConsent().isGiven(); 468 } 469 }) 470 .setEnabledSupplier(flags::getEnableMddEncryptionKeys) 471 .setBackgroundExecutor(AdServicesExecutors.getBackgroundExecutor()) 472 .setFileDownloader(() -> fileDownloader) 473 .setFileStorage(fileStorage) 474 .setManifestFileFlagSupplier(() -> manifestFileFlag) 475 .setManifestConfigParser(manifestConfigFileParser) 476 .setMetadataStore( 477 SharedPreferencesManifestFileMetadata.createFromContext( 478 context, /*InstanceId*/ 479 Optional.absent(), 480 AdServicesExecutors.getBackgroundExecutor())) 481 // TODO(b/239265537): Enable dedup using etag. 482 .setDedupDownloadWithEtag(false) 483 // TODO(b/243829623): use proper Logger. 484 .setLogger( 485 new Logger() { 486 @Override 487 public void log(MessageLite event, int eventCode) { 488 // A no-op logger. 489 } 490 }) 491 .build(); 492 } 493 494 // Check the feature flag is on or off. True means use MddLogger. 495 @VisibleForTesting 496 static Optional<Logger> getMddLogger(Flags flags) { 497 return flags.getMddLoggerEnabled() ? Optional.of(new MddLogger()) : Optional.absent(); 498 } 499 500 /** Dump MDD Debug Info. */ 501 public static void dump(PrintWriter writer) { 502 String debugString = 503 MobileDataDownloadFactory.getMdd(FlagsFactory.getFlags()).getDebugInfoAsString(); 504 writer.println("***====*** MDD Lib dump: ***====***"); 505 506 for (int i = 0; i <= debugString.length() / MAX_ADB_LOGCAT_SIZE; i++) { 507 int start = i * MAX_ADB_LOGCAT_SIZE; 508 int end = (i + 1) * MAX_ADB_LOGCAT_SIZE; 509 end = Math.min(debugString.length(), end); 510 writer.println(debugString.substring(start, end)); 511 } 512 } 513 } 514