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