/* * Copyright (C) 2023 The Android Open Source Project * * 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.android.ondevicepersonalization.services.download.mdd; import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.ondevicepersonalization.internal.util.LoggerFactory; import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors; import com.google.android.downloader.AndroidDownloaderLogger; import com.google.android.downloader.ConnectivityHandler; import com.google.android.downloader.DownloadConstraints; import com.google.android.downloader.Downloader; import com.google.android.downloader.PlatformUrlEngine; import com.google.android.downloader.UrlEngine; import com.google.android.libraries.mobiledatadownload.DownloadException; import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest; import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; import com.google.android.libraries.mobiledatadownload.downloader.offroad.ExceptionHandler; import com.google.android.libraries.mobiledatadownload.downloader.offroad.Offroad2FileDownloader; import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadMetadataStore; import com.google.android.libraries.mobiledatadownload.file.integration.downloader.SharedPreferencesDownloadMetadata; import com.google.common.base.Optional; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.concurrent.Executor; /** * A OnDevicePersonalization custom {@link FileDownloader} */ public class OnDevicePersonalizationFileDownloader implements FileDownloader { private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); private static final String TAG = "OnDevicePersonalizationFileDownloader"; /** Downloader Connection Timeout in Milliseconds. */ private static final int DOWNLOADER_CONNECTION_TIMEOUT_MS = 10 * 1000; // 10 seconds /** Downloader Read Timeout in Milliseconds. */ private static final int DOWNLOADER_READ_TIMEOUT_MS = 10 * 1000; // 10 seconds. /** Downloader max download threads. */ private static final int DOWNLOADER_MAX_DOWNLOAD_THREADS = 2; private static final String MDD_METADATA_SHARED_PREFERENCES = "mdd_metadata_store"; private final SynchronousFileStorage mFileStorage; private final Context mContext; private final Executor mDownloadExecutor; private final FileDownloader mOffroad2FileDownloader; private final FileDownloader mLocalFileDownloader; public OnDevicePersonalizationFileDownloader( SynchronousFileStorage fileStorage, Executor downloadExecutor, Context context) { this.mFileStorage = fileStorage; this.mDownloadExecutor = downloadExecutor; this.mContext = context; this.mOffroad2FileDownloader = getOffroad2FileDownloader(mContext, mFileStorage, mDownloadExecutor); this.mLocalFileDownloader = new OnDevicePersonalizationLocalFileDownloader(mFileStorage, mDownloadExecutor, mContext); } @NonNull private static FileDownloader getOffroad2FileDownloader( @NonNull Context context, @NonNull SynchronousFileStorage fileStorage, @NonNull Executor downloadExecutor) { DownloadMetadataStore downloadMetadataStore = getDownloadMetadataStore(context); Downloader downloader = new Downloader.Builder() .withIOExecutor(OnDevicePersonalizationExecutors.getBlockingExecutor()) .withConnectivityHandler(new NoOpConnectivityHandler()) .withMaxConcurrentDownloads(DOWNLOADER_MAX_DOWNLOAD_THREADS) .withLogger(new AndroidDownloaderLogger()) .addUrlEngine("https", getUrlEngine()) .build(); return new Offroad2FileDownloader( downloader, fileStorage, downloadExecutor, /* authTokenProvider */ null, downloadMetadataStore, getExceptionHandler(), Optional.absent()); } @NonNull private static ExceptionHandler getExceptionHandler() { return ExceptionHandler.withDefaultHandling(); } @NonNull private static DownloadMetadataStore getDownloadMetadataStore(@NonNull Context context) { SharedPreferences sharedPrefs = context.getSharedPreferences(MDD_METADATA_SHARED_PREFERENCES, Context.MODE_PRIVATE); DownloadMetadataStore downloadMetadataStore = new SharedPreferencesDownloadMetadata( sharedPrefs, OnDevicePersonalizationExecutors.getBackgroundExecutor()); return downloadMetadataStore; } @NonNull private static UrlEngine getUrlEngine() { // TODO(b/219594618): Switch to use CronetUrlEngine. return new PlatformUrlEngine( OnDevicePersonalizationExecutors.getBlockingExecutor(), DOWNLOADER_CONNECTION_TIMEOUT_MS, DOWNLOADER_READ_TIMEOUT_MS); } @Override public ListenableFuture startDownloading(DownloadRequest downloadRequest) { Uri fileUri = downloadRequest.fileUri(); String urlToDownload = downloadRequest.urlToDownload(); sLogger.d(TAG + ": startDownloading; fileUri: " + fileUri + "; urlToDownload: " + urlToDownload); Uri uriToDownload = Uri.parse(urlToDownload); if (uriToDownload == null || fileUri == null) { sLogger.e(TAG + ": Invalid urlToDownload " + urlToDownload); return immediateFailedFuture(new IllegalArgumentException("Invalid urlToDownload")); } // Check for debug enabled package and download url. if (OnDevicePersonalizationLocalFileDownloader.isLocalOdpUri(uriToDownload)) { sLogger.d(TAG + ": Handling debug download url: " + urlToDownload); return mLocalFileDownloader.startDownloading(downloadRequest); } if (!urlToDownload.startsWith("https")) { sLogger.e(TAG + ": File url is not secure: " + urlToDownload); return immediateFailedFuture( DownloadException.builder() .setDownloadResultCode( DownloadException.DownloadResultCode.INSECURE_URL_ERROR) .build()); } return mOffroad2FileDownloader.startDownloading(downloadRequest); } // Connectivity constraints will be checked by JobScheduler/WorkManager instead. @VisibleForTesting static class NoOpConnectivityHandler implements ConnectivityHandler { @Override public ListenableFuture checkConnectivity(DownloadConstraints constraints) { return Futures.immediateVoidFuture(); } } }