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