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