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