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