/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.adservices.ondevicepersonalization; import android.adservices.ondevicepersonalization.aidl.IDataAccessService; import android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.WorkerThread; import android.net.Uri; import android.os.Bundle; import android.os.PersistableBundle; import android.os.RemoteException; import com.android.adservices.ondevicepersonalization.flags.Flags; import com.android.ondevicepersonalization.internal.util.LoggerFactory; import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; /** * Generates event tracking URLs for a request. The service can embed these URLs within the * HTML output as needed. When the HTML is rendered within an ODP WebView, ODP will intercept * requests to these URLs, call * {@code IsolatedWorker#onEvent(EventInput, android.os.OutcomeReceiver)}, and log the returned * output in the EVENTS table. * */ @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED) public class EventUrlProvider { private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); private static final String TAG = EventUrlProvider.class.getSimpleName(); private static final long ASYNC_TIMEOUT_MS = 1000; @NonNull private final IDataAccessService mDataAccessService; /** @hide */ public EventUrlProvider(@NonNull IDataAccessService binder) { mDataAccessService = Objects.requireNonNull(binder); } /** * Creates an event tracking URL that returns the provided response. Returns HTTP Status * 200 (OK) if the response data is not empty. Returns HTTP Status 204 (No Content) if the * response data is empty. * * @param eventParams The data to be passed to * {@code IsolatedWorker#onEvent(EventInput, android.os.OutcomeReceiver)} * when the event occurs. * @param responseData The content to be returned to the WebView when the URL is fetched. * @param mimeType The Mime Type of the URL response. * @return An ODP event URL that can be inserted into a WebView. */ @WorkerThread @NonNull public Uri createEventTrackingUrlWithResponse( @NonNull PersistableBundle eventParams, @Nullable byte[] responseData, @Nullable String mimeType) { final long startTimeMillis = System.currentTimeMillis(); Bundle params = new Bundle(); params.putParcelable(Constants.EXTRA_EVENT_PARAMS, eventParams); params.putByteArray(Constants.EXTRA_RESPONSE_DATA, responseData); params.putString(Constants.EXTRA_MIME_TYPE, mimeType); return getUrl(params, Constants.API_NAME_EVENT_URL_CREATE_WITH_RESPONSE, startTimeMillis); } /** * Creates an event tracking URL that redirects to the provided destination URL when it is * clicked in an ODP webview. * * @param eventParams The data to be passed to * {@code IsolatedWorker#onEvent(EventInput, android.os.OutcomeReceiver)} * when the event occurs * @param destinationUrl The URL to redirect to. * @return An ODP event URL that can be inserted into a WebView. */ @WorkerThread @NonNull public Uri createEventTrackingUrlWithRedirect( @NonNull PersistableBundle eventParams, @Nullable Uri destinationUrl) { final long startTimeMillis = System.currentTimeMillis(); Bundle params = new Bundle(); params.putParcelable(Constants.EXTRA_EVENT_PARAMS, eventParams); params.putString(Constants.EXTRA_DESTINATION_URL, destinationUrl.toString()); return getUrl(params, Constants.API_NAME_EVENT_URL_CREATE_WITH_REDIRECT, startTimeMillis); } @NonNull private Uri getUrl( @NonNull Bundle params, int apiName, long startTimeMillis) { int responseCode = Constants.STATUS_SUCCESS; try { BlockingQueue asyncResult = new ArrayBlockingQueue<>(1); mDataAccessService.onRequest( Constants.DATA_ACCESS_OP_GET_EVENT_URL, params, new IDataAccessServiceCallback.Stub() { @Override public void onSuccess(@NonNull Bundle result) { asyncResult.add(new CallbackResult(result, 0)); } @Override public void onError(int errorCode) { asyncResult.add(new CallbackResult(null, errorCode)); } }); CallbackResult callbackResult = asyncResult.take(); Objects.requireNonNull(callbackResult); if (callbackResult.mErrorCode != 0) { throw new IllegalStateException("Error: " + callbackResult.mErrorCode); } Bundle result = Objects.requireNonNull(callbackResult.mResult); Uri url = Objects.requireNonNull( result.getParcelable(Constants.EXTRA_RESULT, Uri.class)); return url; } catch (InterruptedException | RemoteException e) { responseCode = Constants.STATUS_INTERNAL_ERROR; throw new RuntimeException(e); } finally { try { mDataAccessService.logApiCallStats( apiName, System.currentTimeMillis() - startTimeMillis, responseCode); } catch (Exception e) { sLogger.d(e, TAG + ": failed to log metrics"); } } } private static class CallbackResult { final Bundle mResult; final int mErrorCode; CallbackResult(Bundle result, int errorCode) { mResult = result; mErrorCode = errorCode; } } }