1 /*
2  * Copyright (C) 2022 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 android.adservices.ondevicepersonalization;
18 
19 import android.adservices.ondevicepersonalization.aidl.IDataAccessService;
20 import android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback;
21 import android.annotation.FlaggedApi;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.WorkerThread;
25 import android.net.Uri;
26 import android.os.Bundle;
27 import android.os.PersistableBundle;
28 import android.os.RemoteException;
29 
30 import com.android.adservices.ondevicepersonalization.flags.Flags;
31 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
32 
33 import java.util.Objects;
34 import java.util.concurrent.ArrayBlockingQueue;
35 import java.util.concurrent.BlockingQueue;
36 
37 /**
38  * Generates event tracking URLs for a request. The service can embed these URLs within the
39  * HTML output as needed. When the HTML is rendered within an ODP WebView, ODP will intercept
40  * requests to these URLs, call
41  * {@code IsolatedWorker#onEvent(EventInput, android.os.OutcomeReceiver)}, and log the returned
42  * output in the EVENTS table.
43  *
44  */
45 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
46 public class EventUrlProvider {
47     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
48     private static final String TAG = EventUrlProvider.class.getSimpleName();
49     private static final long ASYNC_TIMEOUT_MS = 1000;
50 
51     @NonNull private final IDataAccessService mDataAccessService;
52 
53     /** @hide */
EventUrlProvider(@onNull IDataAccessService binder)54     public EventUrlProvider(@NonNull IDataAccessService binder) {
55         mDataAccessService = Objects.requireNonNull(binder);
56     }
57 
58     /**
59      * Creates an event tracking URL that returns the provided response. Returns HTTP Status
60      * 200 (OK) if the response data is not empty. Returns HTTP Status 204 (No Content) if the
61      * response data is empty.
62      *
63      * @param eventParams The data to be passed to
64      *     {@code IsolatedWorker#onEvent(EventInput, android.os.OutcomeReceiver)}
65      *     when the event occurs.
66      * @param responseData The content to be returned to the WebView when the URL is fetched.
67      * @param mimeType The Mime Type of the URL response.
68      * @return An ODP event URL that can be inserted into a WebView.
69      */
70     @WorkerThread
createEventTrackingUrlWithResponse( @onNull PersistableBundle eventParams, @Nullable byte[] responseData, @Nullable String mimeType)71     @NonNull public Uri createEventTrackingUrlWithResponse(
72             @NonNull PersistableBundle eventParams,
73             @Nullable byte[] responseData,
74             @Nullable String mimeType) {
75         final long startTimeMillis = System.currentTimeMillis();
76         Bundle params = new Bundle();
77         params.putParcelable(Constants.EXTRA_EVENT_PARAMS, eventParams);
78         params.putByteArray(Constants.EXTRA_RESPONSE_DATA, responseData);
79         params.putString(Constants.EXTRA_MIME_TYPE, mimeType);
80         return getUrl(params, Constants.API_NAME_EVENT_URL_CREATE_WITH_RESPONSE, startTimeMillis);
81     }
82 
83     /**
84      * Creates an event tracking URL that redirects to the provided destination URL when it is
85      * clicked in an ODP webview.
86      *
87      * @param eventParams The data to be passed to
88      *     {@code IsolatedWorker#onEvent(EventInput, android.os.OutcomeReceiver)}
89      *     when the event occurs
90      * @param destinationUrl The URL to redirect to.
91      * @return An ODP event URL that can be inserted into a WebView.
92      */
93     @WorkerThread
createEventTrackingUrlWithRedirect( @onNull PersistableBundle eventParams, @Nullable Uri destinationUrl)94     @NonNull public Uri createEventTrackingUrlWithRedirect(
95             @NonNull PersistableBundle eventParams,
96             @Nullable Uri destinationUrl) {
97         final long startTimeMillis = System.currentTimeMillis();
98         Bundle params = new Bundle();
99         params.putParcelable(Constants.EXTRA_EVENT_PARAMS, eventParams);
100         params.putString(Constants.EXTRA_DESTINATION_URL, destinationUrl.toString());
101         return getUrl(params, Constants.API_NAME_EVENT_URL_CREATE_WITH_REDIRECT, startTimeMillis);
102     }
103 
getUrl( @onNull Bundle params, int apiName, long startTimeMillis)104     @NonNull private Uri getUrl(
105             @NonNull Bundle params, int apiName, long startTimeMillis) {
106         int responseCode = Constants.STATUS_SUCCESS;
107         try {
108             BlockingQueue<CallbackResult> asyncResult = new ArrayBlockingQueue<>(1);
109 
110             mDataAccessService.onRequest(
111                     Constants.DATA_ACCESS_OP_GET_EVENT_URL,
112                     params,
113                     new IDataAccessServiceCallback.Stub() {
114                         @Override
115                         public void onSuccess(@NonNull Bundle result) {
116                             asyncResult.add(new CallbackResult(result, 0));
117                         }
118                         @Override
119                         public void onError(int errorCode) {
120                             asyncResult.add(new CallbackResult(null, errorCode));
121                         }
122                 });
123             CallbackResult callbackResult = asyncResult.take();
124             Objects.requireNonNull(callbackResult);
125             if (callbackResult.mErrorCode != 0) {
126                 throw new IllegalStateException("Error: " + callbackResult.mErrorCode);
127             }
128             Bundle result = Objects.requireNonNull(callbackResult.mResult);
129             Uri url = Objects.requireNonNull(
130                     result.getParcelable(Constants.EXTRA_RESULT, Uri.class));
131             return url;
132         } catch (InterruptedException | RemoteException e) {
133             responseCode = Constants.STATUS_INTERNAL_ERROR;
134             throw new RuntimeException(e);
135         } finally {
136             try {
137                 mDataAccessService.logApiCallStats(
138                         apiName,
139                         System.currentTimeMillis() - startTimeMillis,
140                         responseCode);
141             } catch (Exception e) {
142                 sLogger.d(e, TAG + ": failed to log metrics");
143             }
144         }
145     }
146 
147     private static class CallbackResult {
148         final Bundle mResult;
149         final int mErrorCode;
150 
CallbackResult(Bundle result, int errorCode)151         CallbackResult(Bundle result, int errorCode) {
152             mResult = result;
153             mErrorCode = errorCode;
154         }
155     }
156 }
157