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.webtrigger;
18 
19 import android.adservices.ondevicepersonalization.CalleeMetadata;
20 import android.adservices.ondevicepersonalization.Constants;
21 import android.adservices.ondevicepersonalization.MeasurementWebTriggerEventParamsParcel;
22 import android.adservices.ondevicepersonalization.WebTriggerInputParcel;
23 import android.adservices.ondevicepersonalization.WebTriggerOutputParcel;
24 import android.adservices.ondevicepersonalization.aidl.IIsolatedModelService;
25 import android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback;
26 import android.annotation.NonNull;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.os.Binder;
30 import android.os.Bundle;
31 import android.os.RemoteException;
32 import android.os.SystemClock;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.odp.module.common.Clock;
36 import com.android.odp.module.common.MonotonicClock;
37 import com.android.odp.module.common.PackageUtils;
38 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
39 import com.android.ondevicepersonalization.services.Flags;
40 import com.android.ondevicepersonalization.services.FlagsFactory;
41 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
42 import com.android.ondevicepersonalization.services.data.DataAccessPermission;
43 import com.android.ondevicepersonalization.services.data.DataAccessServiceImpl;
44 import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
45 import com.android.ondevicepersonalization.services.inference.IsolatedModelServiceProvider;
46 import com.android.ondevicepersonalization.services.manifest.AppManifestConfig;
47 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
48 import com.android.ondevicepersonalization.services.policyengine.UserDataAccessor;
49 import com.android.ondevicepersonalization.services.serviceflow.ServiceFlow;
50 import com.android.ondevicepersonalization.services.util.LogUtils;
51 import com.android.ondevicepersonalization.services.util.StatsUtils;
52 
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.common.util.concurrent.ListeningScheduledExecutorService;
59 import com.google.common.util.concurrent.MoreExecutors;
60 
61 import java.util.Objects;
62 import java.util.concurrent.TimeUnit;
63 
64 /**
65  * Handles a Web Trigger Registration.
66  */
67 public class WebTriggerFlow implements ServiceFlow<WebTriggerOutputParcel> {
68     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
69     private static final String TAG = "WebTriggerFlow";
70     private final IRegisterMeasurementEventCallback mCallback;
71     private final long mStartTimeMillis;
72     private final long mServiceEntryTimeMillis;
73     private long mStartServiceTimeMillis;
74 
75     @VisibleForTesting
76     static class Injector {
getExecutor()77         ListeningExecutorService getExecutor() {
78             return OnDevicePersonalizationExecutors.getBackgroundExecutor();
79         }
80 
getClock()81         Clock getClock() {
82             return MonotonicClock.getInstance();
83         }
84 
getFlags()85         Flags getFlags() {
86             return FlagsFactory.getFlags();
87         }
88 
getScheduledExecutor()89         ListeningScheduledExecutorService getScheduledExecutor() {
90             return OnDevicePersonalizationExecutors.getScheduledExecutor();
91         }
92 
93     }
94 
95     @NonNull private final Bundle mParams;
96     @NonNull private final Context mContext;
97     @NonNull private final Injector mInjector;
98     @NonNull private IsolatedModelServiceProvider mModelServiceProvider;
99     private MeasurementWebTriggerEventParamsParcel mServiceParcel;
100 
WebTriggerFlow( @onNull Bundle params, @NonNull Context context, @NonNull IRegisterMeasurementEventCallback callback, long startTimeMillis, long serviceEntryTimeMillis)101     public WebTriggerFlow(
102             @NonNull Bundle params,
103             @NonNull Context context,
104             @NonNull IRegisterMeasurementEventCallback callback,
105             long startTimeMillis,
106             long serviceEntryTimeMillis) {
107         this(params, context, callback, startTimeMillis, serviceEntryTimeMillis, new Injector());
108     }
109 
110     @VisibleForTesting
WebTriggerFlow( @onNull Bundle params, @NonNull Context context, @NonNull IRegisterMeasurementEventCallback callback, long startTimeMillis, long serviceEntryTimeMillis, @NonNull Injector injector)111     WebTriggerFlow(
112             @NonNull Bundle params,
113             @NonNull Context context,
114             @NonNull IRegisterMeasurementEventCallback callback,
115             long startTimeMillis,
116             long serviceEntryTimeMillis,
117             @NonNull Injector injector) {
118         mParams = params;
119         mContext = Objects.requireNonNull(context);
120         mCallback = callback;
121         mStartTimeMillis = startTimeMillis;
122         mServiceEntryTimeMillis = serviceEntryTimeMillis;
123         mInjector = Objects.requireNonNull(injector);
124     }
125 
126     // TO-DO: Add web trigger error codes.
127     @Override
isServiceFlowReady()128     public boolean isServiceFlowReady() {
129         mStartServiceTimeMillis = mInjector.getClock().elapsedRealtime();
130 
131         try {
132             if (getGlobalKillSwitch()) {
133                 sendErrorResult(Constants.STATUS_INTERNAL_ERROR);
134                 return false;
135             }
136 
137             if (!UserPrivacyStatus.getInstance().isMeasurementEnabled()) {
138                 sLogger.d(TAG + ": User control is not given for measurement.");
139                 sendErrorResult(Constants.STATUS_PERSONALIZATION_DISABLED);
140                 return false;
141             }
142 
143             mServiceParcel = Objects.requireNonNull(
144                     mParams.getParcelable(
145                         Constants.EXTRA_MEASUREMENT_WEB_TRIGGER_PARAMS,
146                         MeasurementWebTriggerEventParamsParcel.class));
147 
148             Objects.requireNonNull(mServiceParcel.getDestinationUrl());
149             Objects.requireNonNull(mServiceParcel.getAppPackageName());
150             Objects.requireNonNull(mServiceParcel.getIsolatedService());
151             Objects.requireNonNull(mServiceParcel.getIsolatedService().getPackageName());
152             Objects.requireNonNull(mServiceParcel.getIsolatedService().getClassName());
153 
154             if (mServiceParcel.getDestinationUrl().toString().isBlank()
155                     || mServiceParcel.getAppPackageName().isBlank()
156                     || mServiceParcel.getIsolatedService().getPackageName().isBlank()
157                     || mServiceParcel.getIsolatedService().getClassName().isBlank()) {
158                 sendErrorResult(Constants.STATUS_INTERNAL_ERROR);
159                 return false;
160             }
161 
162             if (mServiceParcel.getCertDigest() != null
163                     && !mServiceParcel.getCertDigest().isBlank()) {
164                 String installedPackageCert = PackageUtils.getCertDigest(
165                         mContext, mServiceParcel.getIsolatedService().getPackageName());
166                 if (!mServiceParcel.getCertDigest().equals(installedPackageCert)) {
167                     sLogger.i(TAG + ": Dropping trigger event due to cert mismatch");
168                     sendErrorResult(Constants.STATUS_INTERNAL_ERROR);
169                     return false;
170                 }
171             }
172 
173             AppManifestConfig config = Objects.requireNonNull(
174                     AppManifestConfigHelper.getAppManifestConfig(
175                             mContext, mServiceParcel.getIsolatedService().getPackageName()));
176             if (!mServiceParcel.getIsolatedService()
177                     .getClassName()
178                     .equals(config.getServiceName())) {
179                 sLogger.d(TAG + ": service class not found");
180                 sendErrorResult(Constants.STATUS_CLASS_NOT_FOUND);
181                 return false;
182             }
183 
184             return true;
185         } catch (Exception e) {
186             sendErrorResult(Constants.STATUS_INTERNAL_ERROR);
187             return false;
188         }
189     }
190 
191     @Override
getService()192     public ComponentName getService() {
193         return mServiceParcel.getIsolatedService();
194     }
195 
196     @Override
getServiceParams()197     public Bundle getServiceParams() {
198         Bundle serviceParams = new Bundle();
199         serviceParams.putParcelable(Constants.EXTRA_INPUT,
200                 new WebTriggerInputParcel.Builder(
201                     mServiceParcel.getDestinationUrl(), mServiceParcel.getAppPackageName(),
202                     mServiceParcel.getEventData())
203                     .build());
204         serviceParams.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER,
205                 new DataAccessServiceImpl(
206                     mServiceParcel.getIsolatedService(),
207                     mContext, /* localDataPermission */ DataAccessPermission.READ_WRITE,
208                     /* eventDataPermission */ DataAccessPermission.READ_ONLY));
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_WEB_TRIGGER,
226                                     val, mInjector.getClock(),
227                                     Constants.STATUS_SUCCESS, mStartServiceTimeMillis);
228                             return val;
229                         },
230                         mInjector.getExecutor()
231                 )
232                 .catchingAsync(
233                         Exception.class,
234                         e -> {
235                             StatsUtils.writeServiceRequestMetrics(
236                                     Constants.API_NAME_SERVICE_ON_WEB_TRIGGER, /* result= */ null,
237                                     mInjector.getClock(),
238                                     Constants.STATUS_INTERNAL_ERROR, mStartServiceTimeMillis);
239                             return Futures.immediateFailedFuture(e);
240                         },
241                         mInjector.getExecutor()
242                 );
243     }
244 
245     @Override
getServiceFlowResultFuture( ListenableFuture<Bundle> runServiceFuture)246     public ListenableFuture<WebTriggerOutputParcel> getServiceFlowResultFuture(
247             ListenableFuture<Bundle> runServiceFuture) {
248         return FluentFuture.from(runServiceFuture)
249                 .transform(
250                     result -> result.getParcelable(
251                         Constants.EXTRA_RESULT, WebTriggerOutputParcel.class),
252                     mInjector.getExecutor())
253                 .transform(
254                         result -> {
255                             writeToLog(mServiceParcel, result);
256                             return result;
257                         },
258                         mInjector.getExecutor())
259                 .withTimeout(
260                         mInjector.getFlags().getIsolatedServiceDeadlineSeconds(),
261                         TimeUnit.SECONDS,
262                         mInjector.getScheduledExecutor()
263                 );
264     }
265 
266     @Override
returnResultThroughCallback( ListenableFuture<WebTriggerOutputParcel> serviceFlowResultFuture)267     public void returnResultThroughCallback(
268             ListenableFuture<WebTriggerOutputParcel>  serviceFlowResultFuture) {
269         Futures.addCallback(
270                 serviceFlowResultFuture,
271                 new FutureCallback<WebTriggerOutputParcel>() {
272                     @Override
273                     public void onSuccess(WebTriggerOutputParcel result) {
274                         sendSuccessResult(result);
275                     }
276 
277                     @Override
278                     public void onFailure(Throwable t) {
279                         sLogger.w(TAG + ": Request failed.", t);
280                         sendErrorResult(Constants.STATUS_INTERNAL_ERROR);
281                     }
282                 },
283                 mInjector.getExecutor());
284     }
285 
286     @Override
cleanUpServiceParams()287     public void cleanUpServiceParams() {
288         mModelServiceProvider.unBindFromModelService();
289     }
290 
writeToLog( MeasurementWebTriggerEventParamsParcel wtparams, WebTriggerOutputParcel result)291     private void writeToLog(
292             MeasurementWebTriggerEventParamsParcel wtparams,
293             WebTriggerOutputParcel result) {
294         sLogger.d(TAG + ": writeToLog() started.");
295         var unused = FluentFuture.from(
296                         LogUtils.writeLogRecords(
297                                 mContext,
298                                 mServiceParcel.getAppPackageName(),
299                                 wtparams.getIsolatedService(),
300                                 result.getRequestLogRecord(),
301                                 result.getEventLogRecords()))
302                 .transform(v -> null, MoreExecutors.newDirectExecutorService());
303     }
304 
getGlobalKillSwitch()305     private boolean getGlobalKillSwitch() {
306         long origId = Binder.clearCallingIdentity();
307         boolean globalKillSwitch = mInjector.getFlags().getGlobalKillSwitch();
308         Binder.restoreCallingIdentity(origId);
309         return globalKillSwitch;
310     }
311 
sendSuccessResult(WebTriggerOutputParcel result)312     private void sendSuccessResult(WebTriggerOutputParcel result) {
313         int responseCode = Constants.STATUS_SUCCESS;
314         try {
315             mCallback.onSuccess(
316                     new CalleeMetadata.Builder()
317                             .setServiceEntryTimeMillis(mServiceEntryTimeMillis)
318                             .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime()).build());
319         } catch (RemoteException e) {
320             responseCode = Constants.STATUS_INTERNAL_ERROR;
321             sLogger.w(TAG + ": Callback error", e);
322         } finally {
323             // TODO(b/327683908) - define enum for notifyMeasurementApi
324         }
325     }
326 
sendErrorResult(int errorCode)327     private void sendErrorResult(int errorCode) {
328         try {
329             mCallback.onError(
330                     errorCode,
331                     new CalleeMetadata.Builder()
332                             .setServiceEntryTimeMillis(mServiceEntryTimeMillis)
333                             .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime()).build());
334         } catch (RemoteException e) {
335             sLogger.w(TAG + ": Callback error", e);
336         } finally {
337             // TODO(b/327683908) - define enum for notifyMeasurementApi
338         }
339     }
340 }
341