blob: 32eb4bd8c7368c9b7a422f68fe122663ee9c0dbb [file] [log] [blame]
/*
* 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.topics;
import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS;
import static android.adservices.common.AdServicesStatusUtils.ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE;
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.RequiresPermission;
import android.annotation.TestApi;
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 android.os.SystemClock;
import android.text.TextUtils;
import com.android.adservices.AdServicesCommon;
import com.android.adservices.LogUtil;
import com.android.adservices.ServiceBinder;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* TopicsManager provides APIs for App and Ad-Sdks to get the user interest topics in a privacy
* preserving way.
*
* <p>The instance of the {@link TopicsManager} can be obtained using {@link
* Context#getSystemService} and {@link TopicsManager} class.
*/
public final class TopicsManager {
/**
* Constant that represents the service name for {@link TopicsManager} to be used in {@link
* android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers}
*
* @hide
*/
public static final String TOPICS_SERVICE = "topics_service";
// When an app calls the Topics API directly, it sets the SDK name to empty string.
static final String EMPTY_SDK = "";
// Default value is true to record SDK's Observation when it calls Topics API.
static final boolean RECORD_OBSERVATION_DEFAULT = true;
private Context mContext;
private ServiceBinder<ITopicsService> mServiceBinder;
/**
* Factory method for creating an instance of TopicsManager.
*
* @param context The {@link Context} to use
* @return A {@link TopicsManager} instance
*/
@NonNull
public static TopicsManager get(@NonNull Context context) {
// On TM+, context.getSystemService() does more than just call constructor.
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
? context.getSystemService(TopicsManager.class)
: new TopicsManager(context);
}
/**
* Create TopicsManager
*
* @hide
*/
public TopicsManager(Context context) {
// In case the TopicsManager is initiated from inside a sdk_sandbox process the fields
// will be immediately rewritten by the initialize method below.
initialize(context);
}
/**
* Initializes {@link TopicsManager} with the given {@code context}.
*
* <p>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 TopicsManager initialize(Context context) {
mContext = context;
mServiceBinder =
ServiceBinder.getServiceBinder(
context,
AdServicesCommon.ACTION_TOPICS_SERVICE,
ITopicsService.Stub::asInterface);
return this;
}
@NonNull
private ITopicsService getService() {
ITopicsService service = mServiceBinder.getService();
if (service == null) {
throw new IllegalStateException(ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE);
}
return service;
}
/**
* Return the topics.
*
* @param getTopicsRequest The request for obtaining Topics.
* @param executor The executor to run callback.
* @param callback The callback that's called after topics are available or an error occurs.
* @throws SecurityException if caller is not authorized to call this API.
* @throws IllegalStateException if this API is not available.
* @throws LimitExceededException if rate limit was reached.
*/
@NonNull
@RequiresPermission(ACCESS_ADSERVICES_TOPICS)
public void getTopics(
@NonNull GetTopicsRequest getTopicsRequest,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<GetTopicsResponse, Exception> callback) {
Objects.requireNonNull(getTopicsRequest);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
CallerMetadata callerMetadata =
new CallerMetadata.Builder()
.setBinderElapsedTimestamp(SystemClock.elapsedRealtime())
.build();
final ITopicsService service = getService();
String sdkName = getTopicsRequest.getAdsSdkName();
String appPackageName = "";
String sdkPackageName = "";
// First check if context is SandboxedSdkContext or not
SandboxedSdkContext sandboxedSdkContext =
SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
if (sandboxedSdkContext != null) {
// This is the case with the Sandbox.
sdkPackageName = sandboxedSdkContext.getSdkPackageName();
appPackageName = sandboxedSdkContext.getClientPackageName();
if (!TextUtils.isEmpty(sdkName)) {
throw new IllegalArgumentException(
"When calling Topics API from Sandbox, caller should not set Ads Sdk Name");
}
String sdkNameFromSandboxedContext = sandboxedSdkContext.getSdkName();
if (null == sdkNameFromSandboxedContext || sdkNameFromSandboxedContext.isEmpty()) {
throw new IllegalArgumentException(
"Sdk Name From SandboxedSdkContext should not be null or empty");
}
sdkName = sdkNameFromSandboxedContext;
} else {
// This is the case without the Sandbox.
if (null == sdkName) {
// When adsSdkName is not set, we assume the App calls the Topics API directly.
// We set the adsSdkName to empty to mark this.
sdkName = EMPTY_SDK;
}
appPackageName = mContext.getPackageName();
}
try {
service.getTopics(
new GetTopicsParam.Builder()
.setAppPackageName(appPackageName)
.setSdkName(sdkName)
.setSdkPackageName(sdkPackageName)
.setShouldRecordObservation(getTopicsRequest.shouldRecordObservation())
.build(),
callerMetadata,
new IGetTopicsCallback.Stub() {
@Override
public void onResult(GetTopicsResult resultParcel) {
executor.execute(
() -> {
if (resultParcel.isSuccess()) {
callback.onResult(
new GetTopicsResponse.Builder(
getTopicList(resultParcel))
.build());
} else {
// TODO: Errors should be returned in onFailure method.
callback.onError(
AdServicesStatusUtils.asException(
resultParcel));
}
});
}
@Override
public void onFailure(int resultCode) {
executor.execute(
() ->
callback.onError(
AdServicesStatusUtils.asException(resultCode)));
}
});
} catch (RemoteException e) {
LogUtil.e(e, "RemoteException");
callback.onError(e);
}
}
private List<Topic> getTopicList(GetTopicsResult resultParcel) {
List<Long> taxonomyVersionsList = resultParcel.getTaxonomyVersions();
List<Long> modelVersionsList = resultParcel.getModelVersions();
List<Integer> topicsCodeList = resultParcel.getTopics();
List<Topic> topicList = new ArrayList<>();
int size = taxonomyVersionsList.size();
for (int i = 0; i < size; i++) {
Topic topic =
new Topic(
taxonomyVersionsList.get(i),
modelVersionsList.get(i),
topicsCodeList.get(i));
topicList.add(topic);
}
return topicList;
}
/**
* 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.
*/
// TODO: change to @VisibleForTesting
@TestApi
public void unbindFromService() {
mServiceBinder.unbindFromService();
}
}