• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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;
18 
19 import android.adservices.ondevicepersonalization.Constants;
20 import android.adservices.ondevicepersonalization.DownloadCompletedOutputParcel;
21 import android.adservices.ondevicepersonalization.DownloadInputParcel;
22 import android.adservices.ondevicepersonalization.aidl.IIsolatedModelService;
23 import android.annotation.NonNull;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.net.Uri;
27 import android.os.Bundle;
28 
29 import com.android.odp.module.common.Clock;
30 import com.android.odp.module.common.MonotonicClock;
31 import com.android.odp.module.common.PackageUtils;
32 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
33 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
34 import com.android.ondevicepersonalization.services.data.DataAccessPermission;
35 import com.android.ondevicepersonalization.services.data.DataAccessServiceImpl;
36 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao;
37 import com.android.ondevicepersonalization.services.data.vendor.VendorData;
38 import com.android.ondevicepersonalization.services.download.mdd.MobileDataDownloadFactory;
39 import com.android.ondevicepersonalization.services.download.mdd.OnDevicePersonalizationFileGroupPopulator;
40 import com.android.ondevicepersonalization.services.federatedcompute.FederatedComputeServiceImpl;
41 import com.android.ondevicepersonalization.services.inference.IsolatedModelServiceProvider;
42 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
43 import com.android.ondevicepersonalization.services.policyengine.UserDataAccessor;
44 import com.android.ondevicepersonalization.services.serviceflow.ServiceFlow;
45 import com.android.ondevicepersonalization.services.util.StatsUtils;
46 
47 import com.google.android.libraries.mobiledatadownload.GetFileGroupRequest;
48 import com.google.android.libraries.mobiledatadownload.MobileDataDownload;
49 import com.google.android.libraries.mobiledatadownload.RemoveFileGroupRequest;
50 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
51 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener;
52 import com.google.common.util.concurrent.FluentFuture;
53 import com.google.common.util.concurrent.FutureCallback;
54 import com.google.common.util.concurrent.Futures;
55 import com.google.common.util.concurrent.ListenableFuture;
56 import com.google.common.util.concurrent.ListeningExecutorService;
57 import com.google.mobiledatadownload.ClientConfigProto;
58 
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.util.ArrayList;
62 import java.util.HashMap;
63 import java.util.List;
64 import java.util.Map;
65 import java.util.Objects;
66 
67 public class DownloadFlow implements ServiceFlow<DownloadCompletedOutputParcel> {
68     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
69     private static final String TAG = DownloadFlow.class.getSimpleName();
70     private final String mPackageName;
71     private final Context mContext;
72     private OnDevicePersonalizationVendorDataDao mDao;
73 
74     @NonNull
75     private IsolatedModelServiceProvider mModelServiceProvider;
76     private long mStartServiceTimeMillis;
77     private ComponentName mService;
78     private ParsedFileContents mParsedFileContents;
79 
80     private final Injector mInjector;
81     private final FutureCallback<DownloadCompletedOutputParcel> mCallback;
82 
83     private static class Injector {
getClock()84         Clock getClock() {
85             return MonotonicClock.getInstance();
86         }
87 
getExecutor()88         ListeningExecutorService getExecutor() {
89             return OnDevicePersonalizationExecutors.getBackgroundExecutor();
90         }
91     }
92 
DownloadFlow(String packageName, Context context, FutureCallback<DownloadCompletedOutputParcel> callback)93     public DownloadFlow(String packageName,
94                         Context context, FutureCallback<DownloadCompletedOutputParcel> callback) {
95         mPackageName = packageName;
96         mContext = context;
97         mCallback = callback;
98         mInjector = new Injector();
99     }
100 
101     @Override
isServiceFlowReady()102     public boolean isServiceFlowReady() {
103         try {
104             mStartServiceTimeMillis = mInjector.getClock().elapsedRealtime();
105 
106             Uri uri = Objects.requireNonNull(getClientFileUri());
107 
108             ParsedFileContents fileContents;
109 
110             SynchronousFileStorage fileStorage = MobileDataDownloadFactory.getFileStorage(mContext);
111             try (InputStream in = fileStorage.open(uri, ReadStreamOpener.create())) {
112                 fileContents = DownloadedFileParser.parseJson(in);
113             } catch (IOException ie) {
114                 sLogger.e(ie, TAG + mPackageName + " Failed to process downloaded JSON file");
115                 onSuccess(null);
116                 return false;
117             }
118 
119             long syncToken = fileContents.getSyncToken();
120             if (syncToken == -1 || !validateSyncToken(syncToken)) {
121                 sLogger.d(TAG + mPackageName
122                         + " downloaded JSON file has invalid syncToken provided");
123                 onSuccess(null);
124                 return false;
125             }
126 
127             var vendorDataMap = fileContents.getVendorDataMap();
128             if (vendorDataMap == null || vendorDataMap.isEmpty()) {
129                 sLogger.d(TAG + mPackageName + " downloaded JSON file has no content provided");
130                 onSuccess(null);
131                 return false;
132             }
133 
134             mDao = OnDevicePersonalizationVendorDataDao.getInstance(mContext, getService(),
135                     PackageUtils.getCertDigest(mContext, mPackageName));
136             long existingSyncToken = mDao.getSyncToken();
137 
138             // If existingToken is greaterThan or equal to the new token, skip as there is
139             // no new data. Mark success to upstream caller for reporting purpose
140             if (existingSyncToken >= syncToken) {
141                 sLogger.d(
142                         TAG
143                                 + ": new syncToken value "
144                                 + syncToken
145                                 + " is not newer than existing token value "
146                                 + existingSyncToken);
147                 onSuccess(null);
148                 return false;
149             }
150 
151             mParsedFileContents = fileContents;
152 
153             return true;
154         } catch (Exception e) {
155             mCallback.onFailure(e);
156             return false;
157         }
158     }
159 
160     @Override
getService()161     public ComponentName getService() {
162         if (mService != null) return mService;
163 
164         mService = ComponentName.createRelative(mPackageName,
165                 AppManifestConfigHelper.getServiceNameFromOdpSettings(mContext, mPackageName));
166         return mService;
167     }
168 
169     @Override
getServiceParams()170     public Bundle getServiceParams() {
171         Bundle serviceParams = new Bundle();
172 
173         serviceParams.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER,
174                 new DataAccessServiceImpl(getService(), mContext,
175                         /* localDataPermission */ DataAccessPermission.READ_WRITE,
176                         /* eventDataPermission */ DataAccessPermission.READ_ONLY));
177 
178         serviceParams.putBinder(Constants.EXTRA_FEDERATED_COMPUTE_SERVICE_BINDER,
179                 new FederatedComputeServiceImpl(getService(), mContext));
180 
181         Map<String, byte[]> downloadedContent = new HashMap<>();
182         for (String key : mParsedFileContents.getVendorDataMap().keySet()) {
183             downloadedContent.put(key, mParsedFileContents.getVendorDataMap().get(key).getData());
184         }
185 
186         DataAccessServiceImpl downloadedContentBinder = new DataAccessServiceImpl(
187                 getService(), mContext, /* remoteData */ downloadedContent,
188                 /* localDataPermission */ DataAccessPermission.DENIED,
189                 /* eventDataPermission */ DataAccessPermission.DENIED);
190 
191         serviceParams.putParcelable(Constants.EXTRA_INPUT,
192                 new DownloadInputParcel.Builder()
193                         .setDataAccessServiceBinder(downloadedContentBinder)
194                         .build());
195 
196         serviceParams.putParcelable(Constants.EXTRA_USER_DATA,
197                 new UserDataAccessor().getUserData());
198 
199         mModelServiceProvider = new IsolatedModelServiceProvider();
200         IIsolatedModelService modelService = mModelServiceProvider.getModelService(mContext);
201         serviceParams.putBinder(Constants.EXTRA_MODEL_SERVICE_BINDER, modelService.asBinder());
202 
203         return serviceParams;
204     }
205 
206     @Override
uploadServiceFlowMetrics(ListenableFuture<Bundle> runServiceFuture)207     public void uploadServiceFlowMetrics(ListenableFuture<Bundle> runServiceFuture) {
208         var unused =
209                 FluentFuture.from(runServiceFuture)
210                         .transform(
211                                 val -> {
212                                     StatsUtils.writeServiceRequestMetrics(
213                                             Constants.API_NAME_SERVICE_ON_DOWNLOAD_COMPLETED,
214                                             mService.getPackageName(),
215                                             val,
216                                             mInjector.getClock(),
217                                             Constants.STATUS_SUCCESS,
218                                             mStartServiceTimeMillis);
219                                     return val;
220                                 },
221                                 mInjector.getExecutor())
222                         .catchingAsync(
223                                 Exception.class,
224                                 e -> {
225                                     StatsUtils.writeServiceRequestMetrics(
226                                             Constants.API_NAME_SERVICE_ON_DOWNLOAD_COMPLETED,
227                                             mService.getPackageName(),
228                                             /* result= */ null,
229                                             mInjector.getClock(),
230                                             Constants.STATUS_INTERNAL_ERROR,
231                                             mStartServiceTimeMillis);
232                                     return Futures.immediateFailedFuture(e);
233                                 },
234                                 mInjector.getExecutor());
235     }
236 
237     @Override
getServiceFlowResultFuture( ListenableFuture<Bundle> runServiceFuture)238     public ListenableFuture<DownloadCompletedOutputParcel> getServiceFlowResultFuture(
239             ListenableFuture<Bundle> runServiceFuture) {
240         return FluentFuture.from(runServiceFuture)
241                 .transform(
242                         result -> {
243                             DownloadCompletedOutputParcel downloadResult =
244                                     result.getParcelable(Constants.EXTRA_RESULT,
245                                             DownloadCompletedOutputParcel.class);
246 
247                             List<String> retainedKeys = downloadResult.getRetainedKeys();
248                             if (retainedKeys == null) {
249                                 // TODO(b/270710021): Determine how to correctly handle null
250                                 //  retainedKeys.
251                                 return null;
252                             }
253 
254                             List<VendorData> filteredList = new ArrayList<>();
255                             for (String key : retainedKeys) {
256                                 if (mParsedFileContents.getVendorDataMap().containsKey(key)) {
257                                     filteredList.add(
258                                             mParsedFileContents.getVendorDataMap().get(key));
259                                 }
260                             }
261 
262                             boolean transactionResult =
263                                     mDao.batchUpdateOrInsertVendorDataTransaction(filteredList,
264                                             retainedKeys, mParsedFileContents.getSyncToken());
265 
266                             sLogger.d(TAG + ": filter and store data completed, transaction"
267                                     + " successful: "
268                                     + transactionResult);
269 
270                             return downloadResult;
271                         },
272                         mInjector.getExecutor())
273                 .catching(
274                         Exception.class,
275                         e -> {
276                             sLogger.e(TAG + ": Processing failed.", e);
277                             return null;
278                         },
279                         mInjector.getExecutor());
280     }
281 
removeFileGroup()282     private ListenableFuture<Boolean> removeFileGroup() throws Exception {
283         MobileDataDownload mdd = MobileDataDownloadFactory.getMdd(mContext);
284         String fileGroupName =
285                 OnDevicePersonalizationFileGroupPopulator.createPackageFileGroupName(
286                         mPackageName, mContext);
287 
288         return mdd.removeFileGroup(RemoveFileGroupRequest.newBuilder()
289                 .setGroupName(fileGroupName).build());
290     }
291 
292     @Override
returnResultThroughCallback( ListenableFuture<DownloadCompletedOutputParcel> serviceFlowResultFuture)293     public void returnResultThroughCallback(
294             ListenableFuture<DownloadCompletedOutputParcel> serviceFlowResultFuture) {
295         try {
296             onSuccess(serviceFlowResultFuture.get());
297         } catch (Exception e) {
298             mCallback.onFailure(e);
299         }
300     }
301 
302     @Override
cleanUpServiceParams()303     public void cleanUpServiceParams() {
304         mModelServiceProvider.unBindFromModelService();
305     }
306 
getClientFileUri()307     private Uri getClientFileUri() throws Exception {
308         MobileDataDownload mdd = MobileDataDownloadFactory.getMdd(mContext);
309 
310         String fileGroupName =
311                 OnDevicePersonalizationFileGroupPopulator.createPackageFileGroupName(
312                         mPackageName, mContext);
313 
314         ClientConfigProto.ClientFileGroup cfg = mdd.getFileGroup(
315                         GetFileGroupRequest.newBuilder()
316                                 .setGroupName(fileGroupName)
317                                 .build())
318                 .get();
319 
320         if (cfg == null || cfg.getStatus() != ClientConfigProto.ClientFileGroup.Status.DOWNLOADED) {
321             sLogger.d(TAG + mPackageName + " has no completed downloads.");
322             // No completed downloads is a valid case. Mark as success and return null.
323             mCallback.onSuccess(null);
324             return null;
325         }
326 
327         // It is currently expected that we will only download a single file per package.
328         if (cfg.getFileCount() != 1) {
329             sLogger.d(TAG + ": package : "
330                     + mPackageName + " has "
331                     + cfg.getFileCount() + " files in the fileGroup");
332             onFailure(new IllegalArgumentException("Invalid file count."));
333             return null;
334         }
335 
336         ClientConfigProto.ClientFile clientFile = cfg.getFile(0);
337         return Uri.parse(clientFile.getFileUri());
338     }
339 
validateSyncToken(long syncToken)340     private static boolean validateSyncToken(long syncToken) {
341         // TODO(b/249813538) Add any additional requirements
342         return syncToken % 3600 == 0;
343     }
344 
onFailure(Exception exception)345     private void onFailure(Exception exception) throws Exception {
346         Futures.addCallback(removeFileGroup(),
347                 new FutureCallback<>() {
348                     @Override
349                     public void onSuccess(Boolean result) {
350                         try {
351                             mCallback.onFailure(exception);
352                         } catch (Exception e) {
353                             mCallback.onFailure(e);
354                         }
355                     }
356 
357                     @Override
358                     public void onFailure(Throwable t) {
359                         mCallback.onFailure(t);
360                     }
361                 }, mInjector.getExecutor());
362     }
363 
onSuccess(DownloadCompletedOutputParcel output)364     private void onSuccess(DownloadCompletedOutputParcel output) throws Exception {
365         Futures.addCallback(removeFileGroup(),
366                 new FutureCallback<>() {
367                     @Override
368                     public void onSuccess(Boolean result) {
369                         try {
370                             mCallback.onSuccess(output);
371                         } catch (Exception e) {
372                             mCallback.onFailure(e);
373                         }
374                     }
375 
376                     @Override
377                     public void onFailure(Throwable t) {
378                         mCallback.onFailure(t);
379                     }
380                 }, mInjector.getExecutor());
381     }
382 }
383