/* * 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.measurement; import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION; import android.adservices.adid.AdId; import android.adservices.adid.AdIdCompatibleManager; import android.adservices.common.AdServicesOutcomeReceiver; import android.adservices.common.AdServicesStatusUtils; import android.adservices.common.CallerMetadata; import android.adservices.common.SandboxedSdkContextUtils; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.app.sdksandbox.SandboxedSdkContext; import android.content.Context; import android.net.Uri; import android.os.RemoteException; import android.os.SystemClock; import android.view.InputEvent; import com.android.adservices.AdServicesCommon; import com.android.adservices.LogUtil; import com.android.adservices.ServiceBinder; import com.android.internal.annotations.VisibleForTesting; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * MeasurementManager provides APIs to manage source and trigger registrations. * * @hide */ public class MeasurementCompatibleManager { private interface MeasurementAdIdCallback { void onAdIdCallback(boolean isAdIdEnabled, @Nullable String adIdValue); } private static final long AD_ID_TIMEOUT_MS = 400; private final Context mContext; private final ServiceBinder mServiceBinder; private AdIdCompatibleManager mAdIdManager; private final Executor mAdIdExecutor = Executors.newCachedThreadPool(); private static final String DEBUG_API_WARNING_MESSAGE = "To enable debug api, include ACCESS_ADSERVICES_AD_ID " + "permission and enable advertising ID under device settings"; /** * This is for test purposes, it helps to mock the adIdManager. * * @hide */ @NonNull public static MeasurementCompatibleManager get(@NonNull Context context) { return new MeasurementCompatibleManager(context); } /** * This is for test purposes, it helps to mock the adIdManager. * * @hide */ @VisibleForTesting @NonNull public static MeasurementCompatibleManager get( @NonNull Context context, @NonNull AdIdCompatibleManager adIdManager) { MeasurementCompatibleManager measurementManager = MeasurementCompatibleManager.get(context); measurementManager.mAdIdManager = adIdManager; return measurementManager; } /** * Create MeasurementCompatibleManager. * * @hide */ private MeasurementCompatibleManager(Context context) { mContext = context; mServiceBinder = ServiceBinder.getServiceBinder( context, AdServicesCommon.ACTION_MEASUREMENT_SERVICE, IMeasurementService.Stub::asInterface); mAdIdManager = new AdIdCompatibleManager(context); } /** * Retrieves an {@link IMeasurementService} implementation * * @hide */ @VisibleForTesting @NonNull public IMeasurementService getService() throws IllegalStateException { IMeasurementService service = mServiceBinder.getService(); if (service == null) { throw new IllegalStateException("Unable to find the service"); } return service; } /** Checks if Ad ID permission is enabled. */ private boolean isAdIdPermissionEnabled(AdId adId) { return !AdId.ZERO_OUT.equals(adId.getAdId()); } /** * Register an attribution source / trigger. * * @hide */ private void register( @NonNull RegistrationRequest registrationRequest, @NonNull IMeasurementService service, @Nullable @CallbackExecutor Executor executor, @Nullable AdServicesOutcomeReceiver callback) { Objects.requireNonNull(registrationRequest); requireExecutorForCallback(executor, callback); String registrationType = "source"; if (registrationRequest.getRegistrationType() == RegistrationRequest.REGISTER_TRIGGER) { registrationType = "trigger"; } LogUtil.d("Registering " + registrationType); try { service.register( registrationRequest, generateCallerMetadataWithCurrentTime(), new IMeasurementCallback.Stub() { @Override public void onResult() { if (callback != null) { executor.execute(() -> callback.onResult(new Object())); } } @Override public void onFailure(MeasurementErrorResponse failureParcel) { if (callback != null) { executor.execute( () -> callback.onError( AdServicesStatusUtils.asException( failureParcel))); } } }); } catch (RemoteException e) { LogUtil.e(e, "RemoteException"); if (callback != null) { executor.execute(() -> callback.onError(new IllegalStateException(e))); } } } /** * Register an attribution source (click or view). * * @param attributionSource the platform issues a request to this URI in order to fetch metadata * associated with the attribution source. The source metadata is stored on device, making * it eligible to be matched to future triggers. * @param inputEvent either an {@link InputEvent} object (for a click event) or null (for a view * event). * @param executor used by callback to dispatch results. * @param callback intended to notify asynchronously the API result. */ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION) public void registerSource( @NonNull Uri attributionSource, @Nullable InputEvent inputEvent, @Nullable @CallbackExecutor Executor executor, @Nullable AdServicesOutcomeReceiver callback) { Objects.requireNonNull(attributionSource); requireExecutorForCallback(executor, callback); IMeasurementService service = getServiceWrapper(executor, callback); if (service == null) { // Error was sent in the callback by getServiceWrapper call LogUtil.d("Measurement service not found"); return; } final RegistrationRequest.Builder builder = new RegistrationRequest.Builder( RegistrationRequest.REGISTER_SOURCE, attributionSource, getAppPackageName(), getSdkPackageName()) .setRequestTime(SystemClock.uptimeMillis()) .setInputEvent(inputEvent); // TODO(b/281546062): Can probably remove isAdIdEnabled, since whether adIdValue is null or // not will determine if adId is enabled. getAdId( (isAdIdEnabled, adIdValue) -> register( builder.setAdIdPermissionGranted(isAdIdEnabled) .setAdIdValue(adIdValue) .build(), service, executor, callback)); } /** * Register attribution sources(click or view) from an app context. This API will not process * any redirects, all registration URLs should be supplied with the request. * * @param request app source registration request * @param executor used by callback to dispatch results * @param callback intended to notify asynchronously the API result */ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION) public void registerSource( @NonNull SourceRegistrationRequest request, @Nullable @CallbackExecutor Executor executor, @Nullable AdServicesOutcomeReceiver callback) { Objects.requireNonNull(request); requireExecutorForCallback(executor, callback); IMeasurementService service = getServiceWrapper(executor, callback); if (service == null) { // Error was sent in the callback by getServiceWrapper call LogUtil.d("Measurement service not found"); return; } CallerMetadata callerMetadata = generateCallerMetadataWithCurrentTime(); IMeasurementCallback measurementCallback = new IMeasurementCallback.Stub() { @Override public void onResult() { if (callback != null) { executor.execute(() -> callback.onResult(new Object())); } } @Override public void onFailure(MeasurementErrorResponse failureParcel) { if (callback != null) { executor.execute( () -> callback.onError( AdServicesStatusUtils.asException( failureParcel))); } } }; final SourceRegistrationRequestInternal.Builder builder = new SourceRegistrationRequestInternal.Builder( request, getAppPackageName(), getSdkPackageName(), SystemClock.uptimeMillis()); getAdId( (isAdIdEnabled, adIdValue) -> { try { LogUtil.d("Registering app sources"); service.registerSource( builder.setAdIdValue(adIdValue).build(), callerMetadata, measurementCallback); } catch (RemoteException e) { LogUtil.e(e, "RemoteException"); if (callback != null) { executor.execute(() -> callback.onError(new IllegalStateException(e))); } } }); } /** * Register an attribution source(click or view) from web context. This API will not process any * redirects, all registration URLs should be supplied with the request. At least one of * appDestination or webDestination parameters are required to be provided. If the registration * is successful, {@code callback}'s {@link AdServicesOutcomeReceiver#onResult} is invoked with * null. In case of failure, a {@link Exception} is sent through {@code callback}'s {@link * AdServicesOutcomeReceiver#onError}. Both success and failure feedback are executed on the * provided {@link Executor}. * * @param request source registration request * @param executor used by callback to dispatch results. * @param callback intended to notify asynchronously the API result. */ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION) public void registerWebSource( @NonNull WebSourceRegistrationRequest request, @Nullable Executor executor, @Nullable AdServicesOutcomeReceiver callback) { Objects.requireNonNull(request); requireExecutorForCallback(executor, callback); IMeasurementService service = getServiceWrapper(executor, callback); if (service == null) { // Error was sent in the callback by getServiceWrapper call LogUtil.d("Measurement service not found"); return; } CallerMetadata callerMetadata = generateCallerMetadataWithCurrentTime(); IMeasurementCallback measurementCallback = new IMeasurementCallback.Stub() { @Override public void onResult() { if (callback != null) { executor.execute(() -> callback.onResult(new Object())); } } @Override public void onFailure(MeasurementErrorResponse failureParcel) { if (callback != null) { executor.execute( () -> callback.onError( AdServicesStatusUtils.asException( failureParcel))); } } }; final WebSourceRegistrationRequestInternal.Builder builder = new WebSourceRegistrationRequestInternal.Builder( request, getAppPackageName(), getSdkPackageName(), SystemClock.uptimeMillis()); getAdId( (isAdIdEnabled, adIdValue) -> registerWebSourceWrapper( builder.setAdIdPermissionGranted(isAdIdEnabled).build(), service, executor, callerMetadata, measurementCallback, callback)); } /** Wrapper method for registerWebSource. */ private void registerWebSourceWrapper( @NonNull WebSourceRegistrationRequestInternal request, @NonNull IMeasurementService service, @Nullable Executor executor, @NonNull CallerMetadata callerMetadata, @NonNull IMeasurementCallback measurementCallback, @Nullable AdServicesOutcomeReceiver callback) { requireExecutorForCallback(executor, callback); try { LogUtil.d("Registering web source"); service.registerWebSource(request, callerMetadata, measurementCallback); } catch (RemoteException e) { LogUtil.e(e, "RemoteException"); if (callback != null) { executor.execute(() -> callback.onError(new IllegalStateException(e))); } } } /** * Register an attribution trigger(click or view) from web context. This API will not process * any redirects, all registration URLs should be supplied with the request. If the registration * is successful, {@code callback}'s {@link AdServicesOutcomeReceiver#onResult} is invoked with * null. In case of failure, a {@link Exception} is sent through {@code callback}'s {@link * AdServicesOutcomeReceiver#onError}. Both success and failure feedback are executed on the * provided {@link Executor}. * * @param request trigger registration request * @param executor used by callback to dispatch results * @param callback intended to notify asynchronously the API result */ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION) public void registerWebTrigger( @NonNull WebTriggerRegistrationRequest request, @Nullable Executor executor, @Nullable AdServicesOutcomeReceiver callback) { Objects.requireNonNull(request); requireExecutorForCallback(executor, callback); IMeasurementService service = getServiceWrapper(executor, callback); if (service == null) { // Error was sent in the callback by getServiceWrapper call LogUtil.d("Measurement service not found"); return; } CallerMetadata callerMetadata = generateCallerMetadataWithCurrentTime(); IMeasurementCallback measurementCallback = new IMeasurementCallback.Stub() { @Override public void onResult() { if (callback != null) { executor.execute(() -> callback.onResult(new Object())); } } @Override public void onFailure(MeasurementErrorResponse failureParcel) { if (callback != null) { executor.execute( () -> callback.onError( AdServicesStatusUtils.asException( failureParcel))); } } }; WebTriggerRegistrationRequestInternal.Builder builder = new WebTriggerRegistrationRequestInternal.Builder( request, getAppPackageName(), getSdkPackageName()); getAdId( (isAdIdEnabled, adIdValue) -> registerWebTriggerWrapper( builder.setAdIdPermissionGranted(isAdIdEnabled).build(), service, executor, callerMetadata, measurementCallback, callback)); } /** Wrapper method for registerWebTrigger. */ private void registerWebTriggerWrapper( @NonNull WebTriggerRegistrationRequestInternal request, @NonNull IMeasurementService service, @Nullable Executor executor, @NonNull CallerMetadata callerMetadata, @NonNull IMeasurementCallback measurementCallback, @Nullable AdServicesOutcomeReceiver callback) { requireExecutorForCallback(executor, callback); try { LogUtil.d("Registering web trigger"); service.registerWebTrigger(request, callerMetadata, measurementCallback); } catch (RemoteException e) { LogUtil.e(e, "RemoteException"); if (callback != null) { executor.execute(() -> callback.onError(new IllegalStateException(e))); } } } /** * Register a trigger (conversion). * * @param trigger the API issues a request to this URI to fetch metadata associated with the * trigger. The trigger metadata is stored on-device, and is eligible to be matched with * sources during the attribution process. * @param executor used by callback to dispatch results. * @param callback intended to notify asynchronously the API result. */ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION) public void registerTrigger( @NonNull Uri trigger, @Nullable @CallbackExecutor Executor executor, @Nullable AdServicesOutcomeReceiver callback) { Objects.requireNonNull(trigger); requireExecutorForCallback(executor, callback); IMeasurementService service = getServiceWrapper(executor, callback); if (service == null) { // Error was sent in the callback by getServiceWrapper call LogUtil.d("Measurement service not found"); return; } final RegistrationRequest.Builder builder = new RegistrationRequest.Builder( RegistrationRequest.REGISTER_TRIGGER, trigger, getAppPackageName(), getSdkPackageName()); // TODO(b/281546062) getAdId( (isAdIdEnabled, adIdValue) -> register( builder.setAdIdPermissionGranted(isAdIdEnabled) .setAdIdValue(adIdValue) .build(), service, executor, callback)); } /** * Delete previously registered data. * * @hide */ private void deleteRegistrations( @NonNull DeletionParam deletionParam, @NonNull @CallbackExecutor Executor executor, @NonNull AdServicesOutcomeReceiver callback) { Objects.requireNonNull(deletionParam); Objects.requireNonNull(executor); Objects.requireNonNull(callback); final IMeasurementService service = getServiceWrapper(executor, callback); if (service == null) { // Error was sent in the callback by getServiceWrapper call LogUtil.d("Measurement service not found"); return; } try { service.deleteRegistrations( deletionParam, generateCallerMetadataWithCurrentTime(), new IMeasurementCallback.Stub() { @Override public void onResult() { executor.execute(() -> callback.onResult(new Object())); } @Override public void onFailure(MeasurementErrorResponse failureParcel) { executor.execute( () -> callback.onError( AdServicesStatusUtils.asException( failureParcel))); } }); } catch (RemoteException e) { LogUtil.e(e, "RemoteException"); executor.execute(() -> callback.onError(new IllegalStateException(e))); } } /** * Delete previous registrations. If the deletion is successful, the callback's {@link * AdServicesOutcomeReceiver#onResult} is invoked with null. In case of failure, a {@link * Exception} is sent through the callback's {@link AdServicesOutcomeReceiver#onError}. Both * success and failure feedback are executed on the provided {@link Executor}. * * @param deletionRequest The request for deleting data. * @param executor The executor to run callback. * @param callback intended to notify asynchronously the API result. */ public void deleteRegistrations( @NonNull DeletionRequest deletionRequest, @NonNull @CallbackExecutor Executor executor, @NonNull AdServicesOutcomeReceiver callback) { deleteRegistrations( new DeletionParam.Builder( deletionRequest.getOriginUris(), deletionRequest.getDomainUris(), deletionRequest.getStart(), deletionRequest.getEnd(), getAppPackageName(), getSdkPackageName()) .setDeletionMode(deletionRequest.getDeletionMode()) .setMatchBehavior(deletionRequest.getMatchBehavior()) .build(), executor, callback); } /** * Get Measurement API status. * *

The callback's {@code Integer} value is one of {@code MeasurementApiState}. * * @param executor used by callback to dispatch results. * @param callback intended to notify asynchronously the API result. */ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION) public void getMeasurementApiStatus( @NonNull @CallbackExecutor Executor executor, @NonNull AdServicesOutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); final IMeasurementService service; try { service = getService(); } catch (IllegalStateException e) { LogUtil.e(e, "Failed to bind to measurement service"); executor.execute( () -> callback.onResult(MeasurementManager.MEASUREMENT_API_STATE_DISABLED)); return; } catch (RuntimeException e) { LogUtil.e(e, "Unknown failure while binding measurement service"); executor.execute(() -> callback.onError(e)); return; } try { service.getMeasurementApiStatus( new StatusParam.Builder(getAppPackageName(), getSdkPackageName()).build(), generateCallerMetadataWithCurrentTime(), new IMeasurementApiStatusCallback.Stub() { @Override public void onResult(int result) { executor.execute(() -> callback.onResult(result)); } }); } catch (RemoteException e) { LogUtil.e(e, "RemoteException"); executor.execute( () -> callback.onResult(MeasurementManager.MEASUREMENT_API_STATE_DISABLED)); } catch (RuntimeException e) { LogUtil.e(e, "Unknown failure while getting measurement status"); executor.execute(() -> callback.onError(e)); } } /** * If the service is in an APK (as opposed to the system service), unbind it from the service to * allow the APK process to die. * * @hide Not sure if we'll need this functionality in the final API. For now, we need it for * performance testing to simulate "cold-start" situations. */ @VisibleForTesting public void unbindFromService() { mServiceBinder.unbindFromService(); } /** Returns the package name of the app from the SDK or app context */ private String getAppPackageName() { SandboxedSdkContext sandboxedSdkContext = SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext); return sandboxedSdkContext == null ? mContext.getPackageName() : sandboxedSdkContext.getClientPackageName(); } /** Returns the package name of the sdk from the SDK or empty if no SDK found */ private String getSdkPackageName() { SandboxedSdkContext sandboxedSdkContext = SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext); return sandboxedSdkContext == null ? "" : sandboxedSdkContext.getSdkPackageName(); } private CallerMetadata generateCallerMetadataWithCurrentTime() { return new CallerMetadata.Builder() .setBinderElapsedTimestamp(SystemClock.elapsedRealtime()) .build(); } /** Get Service wrapper, propagates error to the caller */ @Nullable private IMeasurementService getServiceWrapper( @Nullable @CallbackExecutor Executor executor, @Nullable AdServicesOutcomeReceiver callback) { requireExecutorForCallback(executor, callback); IMeasurementService service = null; try { service = getService(); } catch (RuntimeException e) { LogUtil.e(e, "Failed binding to measurement service"); if (callback != null) { executor.execute(() -> callback.onError(e)); } } return service; } private static void requireExecutorForCallback( Executor executor, AdServicesOutcomeReceiver callback) { if (callback != null && executor == null) { throw new IllegalArgumentException( "Executor should be provided when callback is provided."); } } /* Make AdId call with timeout */ @SuppressLint("MissingPermission") private void getAdId(MeasurementAdIdCallback measurementAdIdCallback) { CountDownLatch countDownLatch = new CountDownLatch(1); AtomicBoolean isAdIdEnabled = new AtomicBoolean(); AtomicReference adIdValue = new AtomicReference<>(); mAdIdManager.getAdId( mAdIdExecutor, new AdServicesOutcomeReceiver<>() { @Override public void onResult(AdId adId) { isAdIdEnabled.set(isAdIdPermissionEnabled(adId)); adIdValue.set(adId.getAdId().equals(AdId.ZERO_OUT) ? null : adId.getAdId()); LogUtil.d("AdId permission enabled %b", isAdIdEnabled.get()); countDownLatch.countDown(); } @Override public void onError(Exception error) { boolean isExpected = error instanceof IllegalStateException || error instanceof SecurityException; if (isExpected) { LogUtil.w(DEBUG_API_WARNING_MESSAGE); } else { LogUtil.w(error, DEBUG_API_WARNING_MESSAGE); } countDownLatch.countDown(); } }); boolean timedOut = false; try { timedOut = !countDownLatch.await(AD_ID_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { LogUtil.w(e, "InterruptedException while waiting for AdId"); } if (timedOut) { LogUtil.w("AdId call timed out"); } measurementAdIdCallback.onAdIdCallback(isAdIdEnabled.get(), adIdValue.get()); } }