1 /* 2 * Copyright (C) 2023 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.ondevicepersonalization.services.download.mdd; 18 19 import static com.google.common.util.concurrent.Futures.immediateFailedFuture; 20 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.net.Uri; 24 25 26 import androidx.annotation.NonNull; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.ondevicepersonalization.internal.util.LoggerFactory; 30 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors; 31 32 import com.google.android.downloader.AndroidDownloaderLogger; 33 import com.google.android.downloader.ConnectivityHandler; 34 import com.google.android.downloader.DownloadConstraints; 35 import com.google.android.downloader.Downloader; 36 import com.google.android.downloader.PlatformUrlEngine; 37 import com.google.android.downloader.UrlEngine; 38 import com.google.android.libraries.mobiledatadownload.DownloadException; 39 import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest; 40 import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; 41 import com.google.android.libraries.mobiledatadownload.downloader.offroad.ExceptionHandler; 42 import com.google.android.libraries.mobiledatadownload.downloader.offroad.Offroad2FileDownloader; 43 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 44 import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadMetadataStore; 45 import com.google.android.libraries.mobiledatadownload.file.integration.downloader.SharedPreferencesDownloadMetadata; 46 import com.google.common.base.Optional; 47 import com.google.common.util.concurrent.Futures; 48 import com.google.common.util.concurrent.ListenableFuture; 49 50 import java.util.concurrent.Executor; 51 52 /** 53 * A OnDevicePersonalization custom {@link FileDownloader} 54 */ 55 public class OnDevicePersonalizationFileDownloader implements FileDownloader { 56 private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); 57 private static final String TAG = "OnDevicePersonalizationFileDownloader"; 58 59 /** Downloader Connection Timeout in Milliseconds. */ 60 private static final int DOWNLOADER_CONNECTION_TIMEOUT_MS = 10 * 1000; // 10 seconds 61 /** Downloader Read Timeout in Milliseconds. */ 62 private static final int DOWNLOADER_READ_TIMEOUT_MS = 10 * 1000; // 10 seconds. 63 /** Downloader max download threads. */ 64 private static final int DOWNLOADER_MAX_DOWNLOAD_THREADS = 2; 65 66 private static final String MDD_METADATA_SHARED_PREFERENCES = "mdd_metadata_store"; 67 68 private final SynchronousFileStorage mFileStorage; 69 private final Context mContext; 70 71 private final Executor mDownloadExecutor; 72 73 private final FileDownloader mOffroad2FileDownloader; 74 private final FileDownloader mLocalFileDownloader; 75 OnDevicePersonalizationFileDownloader( SynchronousFileStorage fileStorage, Executor downloadExecutor, Context context)76 public OnDevicePersonalizationFileDownloader( 77 SynchronousFileStorage fileStorage, Executor downloadExecutor, 78 Context context) { 79 this.mFileStorage = fileStorage; 80 this.mDownloadExecutor = downloadExecutor; 81 this.mContext = context; 82 83 this.mOffroad2FileDownloader = getOffroad2FileDownloader(mContext, mFileStorage, 84 mDownloadExecutor); 85 this.mLocalFileDownloader = new OnDevicePersonalizationLocalFileDownloader(mFileStorage, 86 mDownloadExecutor, mContext); 87 88 } 89 90 @NonNull getOffroad2FileDownloader( @onNull Context context, @NonNull SynchronousFileStorage fileStorage, @NonNull Executor downloadExecutor)91 private static FileDownloader getOffroad2FileDownloader( 92 @NonNull Context context, @NonNull SynchronousFileStorage fileStorage, 93 @NonNull Executor downloadExecutor) { 94 DownloadMetadataStore downloadMetadataStore = getDownloadMetadataStore(context); 95 96 Downloader downloader = 97 new Downloader.Builder() 98 .withIOExecutor(OnDevicePersonalizationExecutors.getBlockingExecutor()) 99 .withConnectivityHandler(new NoOpConnectivityHandler()) 100 .withMaxConcurrentDownloads(DOWNLOADER_MAX_DOWNLOAD_THREADS) 101 .withLogger(new AndroidDownloaderLogger()) 102 .addUrlEngine("https", getUrlEngine()) 103 .build(); 104 105 return new Offroad2FileDownloader( 106 downloader, 107 fileStorage, 108 downloadExecutor, 109 /* authTokenProvider */ null, 110 downloadMetadataStore, 111 getExceptionHandler(), 112 Optional.absent()); 113 } 114 115 @NonNull getExceptionHandler()116 private static ExceptionHandler getExceptionHandler() { 117 return ExceptionHandler.withDefaultHandling(); 118 } 119 120 @NonNull getDownloadMetadataStore(@onNull Context context)121 private static DownloadMetadataStore getDownloadMetadataStore(@NonNull Context context) { 122 SharedPreferences sharedPrefs = 123 context.getSharedPreferences(MDD_METADATA_SHARED_PREFERENCES, Context.MODE_PRIVATE); 124 DownloadMetadataStore downloadMetadataStore = 125 new SharedPreferencesDownloadMetadata( 126 sharedPrefs, OnDevicePersonalizationExecutors.getBackgroundExecutor()); 127 return downloadMetadataStore; 128 } 129 130 @NonNull getUrlEngine()131 private static UrlEngine getUrlEngine() { 132 // TODO(b/219594618): Switch to use CronetUrlEngine. 133 return new PlatformUrlEngine( 134 OnDevicePersonalizationExecutors.getBlockingExecutor(), 135 DOWNLOADER_CONNECTION_TIMEOUT_MS, 136 DOWNLOADER_READ_TIMEOUT_MS); 137 } 138 139 @Override startDownloading(DownloadRequest downloadRequest)140 public ListenableFuture<Void> startDownloading(DownloadRequest downloadRequest) { 141 Uri fileUri = downloadRequest.fileUri(); 142 String urlToDownload = downloadRequest.urlToDownload(); 143 sLogger.d(TAG + ": startDownloading; fileUri: " + fileUri 144 + "; urlToDownload: " + urlToDownload); 145 146 Uri uriToDownload = Uri.parse(urlToDownload); 147 if (uriToDownload == null || fileUri == null) { 148 sLogger.e(TAG + ": Invalid urlToDownload " + urlToDownload); 149 return immediateFailedFuture(new IllegalArgumentException("Invalid urlToDownload")); 150 } 151 152 // Check for debug enabled package and download url. 153 if (OnDevicePersonalizationLocalFileDownloader.isLocalOdpUri(uriToDownload)) { 154 sLogger.d(TAG + ": Handling debug download url: " + urlToDownload); 155 return mLocalFileDownloader.startDownloading(downloadRequest); 156 } 157 158 if (!urlToDownload.startsWith("https")) { 159 sLogger.e(TAG + ": File url is not secure: " + urlToDownload); 160 return immediateFailedFuture( 161 DownloadException.builder() 162 .setDownloadResultCode( 163 DownloadException.DownloadResultCode.INSECURE_URL_ERROR) 164 .build()); 165 } 166 167 return mOffroad2FileDownloader.startDownloading(downloadRequest); 168 } 169 170 // Connectivity constraints will be checked by JobScheduler/WorkManager instead. 171 @VisibleForTesting 172 static class NoOpConnectivityHandler implements ConnectivityHandler { 173 @Override checkConnectivity(DownloadConstraints constraints)174 public ListenableFuture<Void> checkConnectivity(DownloadConstraints constraints) { 175 return Futures.immediateVoidFuture(); 176 } 177 } 178 } 179