/* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.libraries.mobiledatadownload; import android.content.Context; import com.google.android.libraries.mobiledatadownload.account.AccountManagerAccountSource; import com.google.android.libraries.mobiledatadownload.delta.DeltaDecoder; import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; import com.google.android.libraries.mobiledatadownload.internal.dagger.ApplicationContextModule; import com.google.android.libraries.mobiledatadownload.internal.dagger.DaggerStandaloneComponent; import com.google.android.libraries.mobiledatadownload.internal.dagger.DownloaderModule; import com.google.android.libraries.mobiledatadownload.internal.dagger.ExecutorsModule; import com.google.android.libraries.mobiledatadownload.internal.dagger.MainMddLibModule; import com.google.android.libraries.mobiledatadownload.internal.dagger.StandaloneComponent; import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger; import com.google.android.libraries.mobiledatadownload.internal.logging.LogSampler; import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; import com.google.android.libraries.mobiledatadownload.internal.logging.MddEventLogger; import com.google.android.libraries.mobiledatadownload.internal.logging.NoOpEventLogger; import com.google.android.libraries.mobiledatadownload.lite.Downloader; import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor; import com.google.android.libraries.mobiledatadownload.monitor.NetworkUsageMonitor; import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; /** * A builder for {@link MobileDataDownload}. * *

* *

WARNING: Only one object should be built. Otherwise, there may be locking errors on the * underlying database and unnecessary memory consumption. * *

Furthermore, there may be interference between scheduled task. */ public final class MobileDataDownloadBuilder { private static final String TAG = "MobileDataDownloadBuilder"; private final DaggerStandaloneComponent.Builder componentBuilder; private Context context; private ListeningExecutorService controlExecutor; private final List fileGroupPopulatorList = new ArrayList<>(); private Optional taskSchedulerOptional = Optional.absent(); private SynchronousFileStorage fileStorage; private NetworkUsageMonitor networkUsageMonitor; private Optional downloadMonitorOptional = Optional.absent(); private Supplier fileDownloaderSupplier; private Optional deltaDecoderOptional = Optional.absent(); private Optional configurator = Optional.absent(); private Optional loggerOptional = Optional.absent(); private Optional silentFeedbackOptional = Optional.absent(); private Optional instanceIdOptional = Optional.absent(); private Optional> foregroundDownloadServiceClassOptional = Optional.absent(); private Optional flagsOptional = Optional.absent(); private Optional accountSourceOptional = Optional.absent(); private boolean useDefaultAccountSource = true; private Optional customFileGroupValidatorOptional = Optional.absent(); private Optional experimentationConfigOptional = Optional.absent(); public static MobileDataDownloadBuilder newBuilder() { return new MobileDataDownloadBuilder(); } private MobileDataDownloadBuilder() { componentBuilder = DaggerStandaloneComponent.builder(); } @CanIgnoreReturnValue public MobileDataDownloadBuilder setContext(Context context) { this.context = context.getApplicationContext(); return this; } /** * Set Unique Instance ID of this instance of MobileDataDownload. Instance ID must be non-empty * and [a-z] (lower case). * *

Most apps should use @Singleton MDD. If an app wants to use multiple instances of MDD, * please be aware of following caveats: Each instance of MDD will have its own metadata, base * directory, and periodic backbround tasks. There is no sharing and no-dedup between instances. * Please talk to @ before using this. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setInstanceIdOptional(Optional instanceIdOptional) { this.instanceIdOptional = instanceIdOptional; return this; } /** * Set the Control Executor which will manage MDD meta data. * *

NOTE: Control Executor must not be single thread executor otherwise it could lead to * deadlock or other side effects. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setControlExecutor(ListeningExecutorService controlExecutor) { Preconditions.checkNotNull(controlExecutor); // Executor that will execute tasks sequentially. this.controlExecutor = controlExecutor; return this; } /** * Sets a config populator that will be used by MDD to periodically refresh the data file groups. * *

If this is not set, then the client is responsible for refreshing the list of file groups in * MDD as and when they see fit. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder addFileGroupPopulator(FileGroupPopulator fileGroupPopulator) { this.fileGroupPopulatorList.add(fileGroupPopulator); return this; } /** * Add a list of config populator that will be used by MDD to periodically refresh the data file * groups. * *

If this is not set, then the client is responsible for refreshing the list of file groups in * MDD as and when they see fit. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder addFileGroupPopulators( ImmutableList fileGroupPopulators) { this.fileGroupPopulatorList.addAll(fileGroupPopulators); return this; } /** * Set the task scheduler that will be used by MDD to schedule periodic and one-off tasks. Clients * can use GCM, FJD or Work Manager to schedule tasks, and then forward the notification to {@link * MobileDataDownload#handleTask(String)}. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setTaskScheduler(Optional taskSchedulerOptional) { this.taskSchedulerOptional = taskSchedulerOptional; return this; } /** Set the optional Configurator which if present will be used by MDD to configure its flags. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setConfiguratorOptional(Optional configurator) { this.configurator = configurator; return this; } /** Set the optional Logger which if present will be used by MDD to log events. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setLoggerOptional(Optional logger) { this.loggerOptional = logger; return this; } /** Set the flags otherwise default values will be used only. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setFlagsOptional(Optional flags) { this.flagsOptional = flags; return this; } /** * Set the optional SilentFeedback which if present will be used by MDD to send silent feedbacks. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setSilentFeedbackOptional( Optional silentFeedbackOptional) { this.silentFeedbackOptional = silentFeedbackOptional; return this; } /** * Set the MobStore SynchronousFileStorage. Ideally this should be the same object as the one used * by the client app to read files from MDD */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setFileStorage(SynchronousFileStorage fileStorage) { this.fileStorage = fileStorage; return this; } /** * Set the NetworkUsageMonitor. This NetworkUsageMonitor instance must be the same instance that * is registered with SynchronousFileStorage. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setNetworkUsageMonitor(NetworkUsageMonitor networkUsageMonitor) { this.networkUsageMonitor = networkUsageMonitor; return this; } /** * Set the DownloadProgressMonitor. This DownloadProgressMonitor instance must be the same * instance that is registered with SynchronousFileStorage. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setDownloadMonitorOptional( Optional downloadMonitorOptional) { this.downloadMonitorOptional = downloadMonitorOptional; return this; } /** * Set the FileDownloader Supplier. MDD takes in a Supplier of FileDownload to support lazy * instantiation of the FileDownloader */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setFileDownloaderSupplier( Supplier fileDownloaderSupplier) { this.fileDownloaderSupplier = fileDownloaderSupplier; return this; } /** Set the Delta file decoder. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setDeltaDecoderOptional( Optional deltaDecoderOptional) { this.deltaDecoderOptional = deltaDecoderOptional; return this; } /** * Set the Foreground Download Service. This foreground service will keep the download alive even * if the user navigates away from the host app. This ensures long download can finish. * *

If the host needs to use both MDDLite and Full MDD, the Foreground Download Service can be * shared as an optimization. Please talk to @ on how to setup a shared Foreground * Download Service. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setForegroundDownloadServiceOptional( Optional> foregroundDownloadServiceClass) { this.foregroundDownloadServiceClassOptional = foregroundDownloadServiceClass; return this; } /** * Sets the AccountSource that's used to wipeout account-related data at maintenance time. If this * method is not called, an account source based on AccountManager will be injected. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setAccountSourceOptional( Optional accountSourceOptional) { this.accountSourceOptional = accountSourceOptional; useDefaultAccountSource = false; return this; } @CanIgnoreReturnValue public MobileDataDownloadBuilder setCustomFileGroupValidatorOptional( Optional customFileGroupValidatorOptional) { this.customFileGroupValidatorOptional = customFileGroupValidatorOptional; return this; } /** * Sets the ExperimentationConfig that's used when propagating experiment ids to external log * sources. If this is not called, experiment ids are not propagated. See for more * details. */ @CanIgnoreReturnValue public MobileDataDownloadBuilder setExperimentationConfigOptional( Optional experimentationConfigOptional) { this.experimentationConfigOptional = experimentationConfigOptional; return this; } // We use java.util.concurrent.Executor directly to create default Control Executor and // Download Executor. public MobileDataDownload build() { Preconditions.checkNotNull(context); Preconditions.checkNotNull(taskSchedulerOptional); Preconditions.checkNotNull(fileStorage); Preconditions.checkNotNull(networkUsageMonitor); Preconditions.checkNotNull(downloadMonitorOptional); Preconditions.checkNotNull(fileDownloaderSupplier); Preconditions.checkNotNull(customFileGroupValidatorOptional); Executor sequentialControlExecutor = MoreExecutors.newSequentialExecutor(controlExecutor); if (configurator.isPresent()) { // Submit commit task to sequentialControlExecutor to ensure that the commit task finishes // before any other API tasks can run. ListenableFuture commitFuture = PropagatedFutures.submitAsync( () -> configurator.get().commitToFlagSnapshot(), sequentialControlExecutor); PropagatedFutures.addCallback( commitFuture, new FutureCallback() { @Override public void onSuccess(Void result) { LogUtil.d("%s: Succeeded commitToFlagSnapshot.", TAG); } @Override public void onFailure(Throwable t) { LogUtil.w("%s: Failed to commitToFlagSnapshot: %s", TAG, t); } }, MoreExecutors.directExecutor() /*fine to use directExecutor since it only print logs*/); } componentBuilder.applicationContextModule(new ApplicationContextModule(context)); componentBuilder.executorsModule(new ExecutorsModule(sequentialControlExecutor)); componentBuilder.downloaderModule( new DownloaderModule(deltaDecoderOptional, fileDownloaderSupplier)); Flags flags = flagsOptional.or(new Flags() {}); // EventLogger is needed in FrameworkProtoDataStoreModule, which is a sting module. As such it // cannot be constructed in our internal dagger module if we want to share the same EventLogger // throughout the library. final EventLogger eventLogger; if (loggerOptional.isPresent()) { eventLogger = new MddEventLogger( context, loggerOptional.get(), Constants.MDD_LIB_VERSION, new LogSampler(flags, new SecureRandom()), flags); } else { eventLogger = new NoOpEventLogger(); } if (useDefaultAccountSource) { accountSourceOptional = Optional.of(new AccountManagerAccountSource(context)); } componentBuilder.mainMddLibModule( new MainMddLibModule( fileStorage, networkUsageMonitor, eventLogger, downloadMonitorOptional, silentFeedbackOptional, instanceIdOptional, accountSourceOptional, flags, experimentationConfigOptional)); StandaloneComponent component = componentBuilder.build(); if (eventLogger instanceof MddEventLogger) { ((MddEventLogger) eventLogger).setLoggingStateStore(component.getLoggingStateStore()); } Downloader.Builder singleFileDownloaderBuilder = Downloader.newBuilder() .setContext(context) .setControlExecutor(sequentialControlExecutor) .setFileDownloaderSupplier(fileDownloaderSupplier); if (downloadMonitorOptional.isPresent()) { singleFileDownloaderBuilder.setDownloadMonitor(downloadMonitorOptional.get()); } if (foregroundDownloadServiceClassOptional.isPresent()) { singleFileDownloaderBuilder.setForegroundDownloadService( foregroundDownloadServiceClassOptional.get()); } Downloader singleFileDownloader = singleFileDownloaderBuilder.build(); return new MobileDataDownloadImpl( context, component.getEventLogger(), component.getMobileDataDownloadManager(), sequentialControlExecutor, fileGroupPopulatorList, taskSchedulerOptional, fileStorage, downloadMonitorOptional, foregroundDownloadServiceClassOptional, flags, singleFileDownloader, customFileGroupValidatorOptional, component.getTimeSource()); } }