/* * Copyright (C) 2023 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.signals; import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_PROTECTED_SIGNALS; import android.adservices.common.AdServicesStatusUtils; import android.adservices.common.FledgeErrorResponse; import android.adservices.common.SandboxedSdkContextUtils; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.RequiresApi; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.app.sdksandbox.SandboxedSdkContext; import android.content.Context; import android.os.Build; import android.os.LimitExceededException; import android.os.OutcomeReceiver; import android.os.RemoteException; import com.android.adservices.AdServicesCommon; import com.android.adservices.LoggerFactory; import com.android.adservices.ServiceBinder; import com.android.adservices.flags.Flags; import java.util.Objects; import java.util.concurrent.Executor; /** ProtectedSignalsManager provides APIs for apps and ad-SDKs to manage their protected signals. */ @FlaggedApi(Flags.FLAG_PROTECTED_SIGNALS_ENABLED) @RequiresApi(Build.VERSION_CODES.S) public class ProtectedSignalsManager { private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); /** * Constant that represents the service name for {@link ProtectedSignalsManager} to be used in * {@link android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers} * * @hide */ public static final String PROTECTED_SIGNALS_SERVICE = "protected_signals_service"; @NonNull private Context mContext; @NonNull private ServiceBinder mServiceBinder; /** * Factory method for creating an instance of ProtectedSignalsManager. * * @param context The {@link Context} to use * @return A {@link ProtectedSignalsManager} instance */ @SuppressLint("ManagerLookup") @NonNull // TODO(b/303896680): Investigate why this lint was not triggered for similar managers public static ProtectedSignalsManager get(@NonNull Context context) { // On T+, context.getSystemService() does more than just call constructor. return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) ? context.getSystemService(ProtectedSignalsManager.class) : new ProtectedSignalsManager(context); } /** * Create a service binder ProtectedSignalsManager * * @hide */ public ProtectedSignalsManager(@NonNull Context context) { Objects.requireNonNull(context); // In case the ProtectedSignalsManager is initiated from inside a sdk_sandbox process the // fields will be immediately rewritten by the initialize method below. initialize(context); } /** * Initializes {@link ProtectedSignalsManager} with the given {@code context}. * *

This method is called by the {@link SandboxedSdkContext} to propagate the correct context. * For more information check the javadoc on the {@link * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}. * * @hide * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry */ public ProtectedSignalsManager initialize(@NonNull Context context) { Objects.requireNonNull(context); mContext = context; mServiceBinder = ServiceBinder.getServiceBinder( context, AdServicesCommon.ACTION_PROTECTED_SIGNALS_SERVICE, IProtectedSignalsService.Stub::asInterface); return this; } @NonNull IProtectedSignalsService getService() { IProtectedSignalsService service = mServiceBinder.getService(); if (service == null) { throw new IllegalStateException("Unable to find the service"); } return service; } /** * The updateSignals API will retrieve a JSON from the URI that describes which signals to add * or remove. This API also allows registering the encoder endpoint. The endpoint is used to * download an encoding logic, which enables encoding the signals. * *

The top level keys for the JSON must correspond to one of 5 commands: * *

"put" - Adds a new signal, overwriting any existing signals with the same key. The value * for this is a JSON object where the keys are base 64 strings corresponding to the key to put * for and the values are base 64 string corresponding to the value to put. * *

"append" - Appends a new signal/signals to a time series of signals, removing the oldest * signals to make room for the new ones if the size of the series exceeds the given maximum. * The value for this is a JSON object where the keys are base 64 strings corresponding to the * key to append to and the values are objects with two fields: "values" and "maxSignals" . * "values" is a list of base 64 strings corresponding to signal values to append to the time * series. "maxSignals" is the maximum number of values that are allowed in this timeseries. If * the current number of signals associated with the key exceeds maxSignals the oldest signals * will be removed. Note that you can append to a key added by put. Not that appending more than * the maximum number of values will cause a failure. * *

"put_if_not_present" - Adds a new signal only if there are no existing signals with the * same key. The value for this is a JSON object where the keys are base 64 strings * corresponding to the key to put for and the values are base 64 string corresponding to the * value to put. * *

"remove" - Removes the signal for a key. The value of this is a list of base 64 strings * corresponding to the keys of signals that should be deleted. * *

"update_encoder" - Provides an action to update the endpoint, and a URI which can be used * to retrieve an encoding logic. The sub-key for providing an update action is "action" and the * values currently supported are: * *

    *
  1. "REGISTER" : Registers the encoder endpoint if provided for the first time or * overwrites the existing one with the newly provided endpoint. Providing the "endpoint" * is required for the "REGISTER" action. *
* *

The sub-key for providing an encoder endpoint is "endpoint" and the value is the URI * string for the endpoint. * *

On success, the onResult method of the provided OutcomeReceiver will be called with an * empty Object. This Object has no significance and is used merely as a placeholder. * *

Key may only be operated on by one command per JSON. If two command attempt to operate on * the same key, this method will through an {@link IllegalArgumentException} * *

This call fails with an {@link SecurityException} if * *

    *
  1. the {@code ownerPackageName} is not calling app's package name and/or *
  2. the buyer is not authorized to use the API. *
* *

This call fails with an {@link IllegalArgumentException} if * *

    *
  1. The JSON retrieved from the server is not valid. *
  2. The provided URI is invalid. *
* *

This call fails with {@link LimitExceededException} if the calling package exceeds the * allowed rate limits and is throttled. * *

This call fails with an {@link IllegalStateException} if an internal service error is * encountered. */ @RequiresPermission(ACCESS_ADSERVICES_PROTECTED_SIGNALS) public void updateSignals( @NonNull UpdateSignalsRequest updateSignalsRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver receiver) { Objects.requireNonNull(updateSignalsRequest); Objects.requireNonNull(executor); Objects.requireNonNull(receiver); try { final IProtectedSignalsService service = getService(); service.updateSignals( new UpdateSignalsInput.Builder( updateSignalsRequest.getUpdateUri(), getCallerPackageName()) .build(), new UpdateSignalsCallback.Stub() { @Override public void onSuccess() { executor.execute(() -> receiver.onResult(new Object())); } @Override public void onFailure(FledgeErrorResponse failureParcel) { executor.execute( () -> receiver.onError( AdServicesStatusUtils.asException( failureParcel))); } }); } catch (RemoteException e) { sLogger.e(e, "Exception"); receiver.onError(new IllegalStateException("Internal Error!", e)); } } private String getCallerPackageName() { SandboxedSdkContext sandboxedSdkContext = SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext); return sandboxedSdkContext == null ? mContext.getPackageName() : sandboxedSdkContext.getClientPackageName(); } }