blob: f15585624f7b26ec3879082b371ca5d66252b644 [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 com.android.adservices.service.topics;
import static android.adservices.common.AdServicesStatusUtils.STATUS_BACKGROUND_CALLER;
import static android.adservices.common.AdServicesStatusUtils.STATUS_CALLER_NOT_ALLOWED;
import static android.adservices.common.AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
import static android.adservices.common.AdServicesStatusUtils.STATUS_PERMISSION_NOT_REQUESTED;
import static android.adservices.common.AdServicesStatusUtils.STATUS_RATE_LIMIT_REACHED;
import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
import static android.adservices.common.AdServicesStatusUtils.STATUS_UNAUTHORIZED;
import static android.adservices.common.AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_CLASS__TARGETING;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__GET_TOPICS;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__GET_TOPICS_PREVIEW_API;
import android.adservices.common.AdServicesStatusUtils;
import android.adservices.common.CallerMetadata;
import android.adservices.topics.GetTopicsParam;
import android.adservices.topics.IGetTopicsCallback;
import android.adservices.topics.ITopicsService;
import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Process;
import android.os.RemoteException;
import android.text.TextUtils;
import com.android.adservices.LogUtil;
import com.android.adservices.concurrency.AdServicesExecutors;
import com.android.adservices.data.enrollment.EnrollmentDao;
import com.android.adservices.service.Flags;
import com.android.adservices.service.common.AllowLists;
import com.android.adservices.service.common.AppImportanceFilter;
import com.android.adservices.service.common.AppImportanceFilter.WrongCallingApplicationStateException;
import com.android.adservices.service.common.AppManifestConfigHelper;
import com.android.adservices.service.common.PermissionHelper;
import com.android.adservices.service.common.SdkRuntimeUtil;
import com.android.adservices.service.common.Throttler;
import com.android.adservices.service.consent.AdServicesApiConsent;
import com.android.adservices.service.consent.AdServicesApiType;
import com.android.adservices.service.consent.ConsentManager;
import com.android.adservices.service.enrollment.EnrollmentData;
import com.android.adservices.service.stats.AdServicesLogger;
import com.android.adservices.service.stats.AdServicesStatsLog;
import com.android.adservices.service.stats.ApiCallStats;
import com.android.adservices.service.stats.Clock;
import java.util.concurrent.Executor;
/**
* Implementation of {@link ITopicsService}.
*
* @hide
*/
public class TopicsServiceImpl extends ITopicsService.Stub {
private static final Executor sBackgroundExecutor = AdServicesExecutors.getBackgroundExecutor();
private final Context mContext;
private final TopicsWorker mTopicsWorker;
private final AdServicesLogger mAdServicesLogger;
private final ConsentManager mConsentManager;
private final Clock mClock;
private final Flags mFlags;
private final Throttler mThrottler;
private final EnrollmentDao mEnrollmentDao;
private final AppImportanceFilter mAppImportanceFilter;
public TopicsServiceImpl(
Context context,
TopicsWorker topicsWorker,
ConsentManager consentManager,
AdServicesLogger adServicesLogger,
Clock clock,
Flags flags,
Throttler throttler,
EnrollmentDao enrollmentDao,
AppImportanceFilter appImportanceFilter) {
mContext = context;
mTopicsWorker = topicsWorker;
mConsentManager = consentManager;
mAdServicesLogger = adServicesLogger;
mClock = clock;
mFlags = flags;
mThrottler = throttler;
mEnrollmentDao = enrollmentDao;
mAppImportanceFilter = appImportanceFilter;
}
@Override
public void getTopics(
@NonNull GetTopicsParam topicsParam,
@NonNull CallerMetadata callerMetadata,
@NonNull IGetTopicsCallback callback) {
if (isThrottled(topicsParam, callback)) return;
final long startServiceTime = mClock.elapsedRealtime();
// TODO(b/236380919): Verify that the passed App PackageName belongs to the caller uid
final String packageName = topicsParam.getAppPackageName();
final String sdkName = topicsParam.getSdkName();
final String sdkPackageName = topicsParam.getSdkPackageName();
// We need to save the Calling Uid before offloading to the background executor. Otherwise,
// the Binder.getCallingUid will return the PPAPI process Uid. This also needs to be final
// since it's used in the lambda.
final int callingUid = Binder.getCallingUidOrThrow();
// Check the permission in the same thread since we're looking for caller's permissions.
// Note: The permission check uses sdk package name since PackageManager checks if the
// permission is declared in the manifest of that package name.
boolean hasTopicsPermission =
PermissionHelper.hasTopicsPermission(
mContext, Process.isSdkSandboxUid(callingUid), sdkPackageName);
sBackgroundExecutor.execute(
() -> {
int resultCode = STATUS_SUCCESS;
try {
if (!canCallerInvokeTopicsService(
hasTopicsPermission, topicsParam, callingUid, callback)) {
return;
}
callback.onResult(mTopicsWorker.getTopics(packageName, sdkName));
if (topicsParam.shouldRecordObservation()) {
mTopicsWorker.recordUsage(
topicsParam.getAppPackageName(), topicsParam.getSdkName());
}
} catch (RemoteException e) {
LogUtil.e(e, "Unable to send result to the callback");
resultCode = STATUS_INTERNAL_ERROR;
} finally {
long binderCallStartTimeMillis = callerMetadata.getBinderElapsedTimestamp();
long serviceLatency = mClock.elapsedRealtime() - startServiceTime;
// Double it to simulate the return binder time is same to call binder time
long binderLatency = (startServiceTime - binderCallStartTimeMillis) * 2;
final int apiLatency = (int) (serviceLatency + binderLatency);
mAdServicesLogger.logApiCallStats(
new ApiCallStats.Builder()
.setCode(AdServicesStatsLog.AD_SERVICES_API_CALLED)
.setApiClass(AD_SERVICES_API_CALLED__API_CLASS__TARGETING)
.setApiName(
topicsParam.shouldRecordObservation()
? AD_SERVICES_API_CALLED__API_NAME__GET_TOPICS
: AD_SERVICES_API_CALLED__API_NAME__GET_TOPICS_PREVIEW_API)
.setAppPackageName(packageName)
.setSdkPackageName(sdkName)
.setLatencyMillisecond(apiLatency)
.setResultCode(resultCode)
.build());
}
});
}
// Throttle the Topics API.
// Return true if we should throttle (don't allow the API call).
private boolean isThrottled(GetTopicsParam topicsParam, IGetTopicsCallback callback) {
// There are 2 cases for throttling:
// Case 1: the App calls Topics API directly, not via an SDK. In this case,
// the SdkName == Empty
// Case 2: the SDK calls Topics API.
boolean throttled =
TextUtils.isEmpty(topicsParam.getSdkName())
? !mThrottler.tryAcquire(
Throttler.ApiKey.TOPICS_API_APP_PACKAGE_NAME,
topicsParam.getAppPackageName())
: !mThrottler.tryAcquire(
Throttler.ApiKey.TOPICS_API_SDK_NAME, topicsParam.getSdkName());
if (throttled) {
LogUtil.e("Rate Limit Reached for TOPICS_API");
try {
callback.onFailure(STATUS_RATE_LIMIT_REACHED);
} catch (RemoteException e) {
LogUtil.e(e, "Fail to call the callback on Rate Limit Reached.");
}
return true;
}
return false;
}
// Enforce whether caller is from foreground.
private void enforceForeground(int callingUid, @NonNull String sdkName) {
// If caller calls Topics API from Sandbox, regard it as foreground.
// Also enable a flag to force switch on/off this enforcing.
if (Process.isSdkSandboxUid(callingUid) || !mFlags.getEnforceForegroundStatusForTopics()) {
return;
}
// Call utility method in AppImportanceFilter to enforce foreground status
// Throw WrongCallingApplicationStateException if the assertion fails.
mAppImportanceFilter.assertCallerIsInForeground(
callingUid, AD_SERVICES_API_CALLED__API_NAME__GET_TOPICS, sdkName);
}
/**
* Check whether caller can invoke the Topics API. The caller is not allowed to do it when one
* of the following occurs:
*
* <ul>
* <li>Permission was not requested.
* <li>Caller is not allowed - not present in the allowed list.
* <li>User consent was revoked.
* </ul>
*
* @param sufficientPermission boolean which tells whether caller has sufficient permissions.
* @param topicsParam {@link GetTopicsParam} to get information about the request.
* @param callback {@link IGetTopicsCallback} to invoke when caller is not allowed.
* @return true if caller is allowed to invoke Topics API, false otherwise.
*/
private boolean canCallerInvokeTopicsService(
boolean sufficientPermission,
GetTopicsParam topicsParam,
int callingUid,
IGetTopicsCallback callback) {
// Enforce caller calls Topics API from foreground
try {
enforceForeground(callingUid, topicsParam.getSdkName());
} catch (WrongCallingApplicationStateException backgroundCaller) {
invokeCallbackWithStatus(
callback, STATUS_BACKGROUND_CALLER, backgroundCaller.getMessage());
return false;
}
if (!sufficientPermission) {
invokeCallbackWithStatus(
callback,
STATUS_PERMISSION_NOT_REQUESTED,
"Unauthorized caller. Permission not requested.");
return false;
}
// This needs to access PhFlag which requires READ_DEVICE_CONFIG which
// is not granted for binder thread. So we have to check it with one
// of non-binder thread of the PPAPI.
if (!AllowLists.isSignatureAllowListed(
mContext,
mFlags.getPpapiAppSignatureAllowList(),
topicsParam.getAppPackageName())) {
invokeCallbackWithStatus(
callback,
STATUS_CALLER_NOT_ALLOWED,
"Unauthorized caller. Signatures for calling package not allowed.");
return false;
}
// Check whether calling package belongs to the callingUid
int resultCode =
enforceCallingPackageBelongsToUid(topicsParam.getAppPackageName(), callingUid);
if (resultCode != STATUS_SUCCESS) {
invokeCallbackWithStatus(callback, resultCode, "Caller is not authorized.");
return false;
}
AdServicesApiConsent userConsent;
if (mFlags.getGaUxFeatureEnabled()) {
userConsent = mConsentManager.getConsent(AdServicesApiType.TOPICS);
} else {
userConsent = mConsentManager.getConsent();
}
if (!userConsent.isGiven()) {
invokeCallbackWithStatus(
callback, STATUS_USER_CONSENT_REVOKED, "User consent revoked.");
return false;
}
// The app developer declares which SDKs they would like to allow Topics
// access to use the enrollment ID. Get the enrollment ID for this SDK and
// check that against the app's manifest.
if (!mFlags.isDisableTopicsEnrollmentCheck() && !topicsParam.getSdkName().isEmpty()) {
EnrollmentData enrollmentData =
mEnrollmentDao.getEnrollmentDataFromSdkName(topicsParam.getSdkName());
boolean permitted =
(enrollmentData != null && enrollmentData.getEnrollmentId() != null)
&& AppManifestConfigHelper.isAllowedTopicsAccess(
mContext,
Process.isSdkSandboxUid(callingUid),
topicsParam.getAppPackageName(),
enrollmentData.getEnrollmentId())
&& !mFlags.isEnrollmentBlocklisted(enrollmentData.getEnrollmentId());
if (!permitted) {
invokeCallbackWithStatus(
callback, STATUS_CALLER_NOT_ALLOWED, "Caller is not authorized.");
return false;
}
}
return true;
}
private void invokeCallbackWithStatus(
IGetTopicsCallback callback,
@AdServicesStatusUtils.StatusCode int statusCode,
String message) {
LogUtil.e(message);
try {
callback.onFailure(statusCode);
} catch (RemoteException e) {
LogUtil.e(e, String.format("Fail to call the callback. %s", message));
}
}
// Enforce that the callingPackage has the callingUid.
private int enforceCallingPackageBelongsToUid(String callingPackage, int callingUid) {
int appCallingUid = SdkRuntimeUtil.getCallingAppUid(callingUid);
int packageUid;
try {
packageUid = mContext.getPackageManager().getPackageUid(callingPackage, /* flags */ 0);
} catch (PackageManager.NameNotFoundException e) {
LogUtil.e(e, callingPackage + " not found");
return STATUS_UNAUTHORIZED;
}
if (packageUid != appCallingUid) {
LogUtil.e(callingPackage + " does not belong to uid " + callingUid);
return STATUS_UNAUTHORIZED;
}
return STATUS_SUCCESS;
}
/** Init the Topics Service. */
public void init() {
// This is to prevent cold-start latency on getTopics API.
// Load cache when the service is created.
// The recommended pattern is:
// 1) In app startup, wake up the TopicsService.
// 2) The TopicsService will load the Topics Cache from DB into memory.
// 3) Later, when the app calls Topics API, the returned Topics will be served
// from
// Cache in memory.
sBackgroundExecutor.execute(mTopicsWorker::loadCache);
}
}