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