/*
* Copyright (C) 2020 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.server.appsearch;
import static android.app.appsearch.AppSearchResult.RESULT_DENIED;
import static android.app.appsearch.AppSearchResult.RESULT_INTERNAL_ERROR;
import static android.app.appsearch.AppSearchResult.RESULT_INVALID_ARGUMENT;
import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND;
import static android.app.appsearch.AppSearchResult.RESULT_OK;
import static android.app.appsearch.AppSearchResult.RESULT_RATE_LIMITED;
import static android.app.appsearch.AppSearchResult.RESULT_SECURITY_ERROR;
import static android.app.appsearch.AppSearchResult.RESULT_TIMED_OUT;
import static android.app.appsearch.AppSearchResult.throwableToFailedResult;
import static android.app.appsearch.functions.AppFunctionManager.PERMISSION_BIND_APP_FUNCTION_SERVICE;
import static android.os.Process.INVALID_UID;
import static com.android.server.appsearch.external.localstorage.stats.SearchStats.VISIBILITY_SCOPE_GLOBAL;
import static com.android.server.appsearch.external.localstorage.stats.SearchStats.VISIBILITY_SCOPE_LOCAL;
import static com.android.server.appsearch.util.ServiceImplHelper.invokeCallbackOnError;
import static com.android.server.appsearch.util.ServiceImplHelper.invokeCallbackOnResult;
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.annotation.WorkerThread;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchEnvironment;
import android.app.appsearch.AppSearchEnvironmentFactory;
import android.app.appsearch.AppSearchMigrationHelper;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.GetSchemaResponse;
import android.app.appsearch.InternalSetSchemaResponse;
import android.app.appsearch.SearchResultPage;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.SearchSuggestionResult;
import android.app.appsearch.SetSchemaResponse;
import android.app.appsearch.SetSchemaResponse.MigrationFailure;
import android.app.appsearch.StorageInfo;
import android.app.appsearch.aidl.AppSearchBatchResultParcel;
import android.app.appsearch.aidl.AppSearchResultParcel;
import android.app.appsearch.aidl.ExecuteAppFunctionAidlRequest;
import android.app.appsearch.aidl.GetDocumentsAidlRequest;
import android.app.appsearch.aidl.GetNamespacesAidlRequest;
import android.app.appsearch.aidl.GetNextPageAidlRequest;
import android.app.appsearch.aidl.GetSchemaAidlRequest;
import android.app.appsearch.aidl.GetStorageInfoAidlRequest;
import android.app.appsearch.aidl.GlobalSearchAidlRequest;
import android.app.appsearch.aidl.IAppFunctionService;
import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
import android.app.appsearch.aidl.IAppSearchManager;
import android.app.appsearch.aidl.IAppSearchObserverProxy;
import android.app.appsearch.aidl.IAppSearchResultCallback;
import android.app.appsearch.aidl.InitializeAidlRequest;
import android.app.appsearch.aidl.InvalidateNextPageTokenAidlRequest;
import android.app.appsearch.aidl.PersistToDiskAidlRequest;
import android.app.appsearch.aidl.PutDocumentsAidlRequest;
import android.app.appsearch.aidl.PutDocumentsFromFileAidlRequest;
import android.app.appsearch.aidl.RegisterObserverCallbackAidlRequest;
import android.app.appsearch.aidl.RemoveByDocumentIdAidlRequest;
import android.app.appsearch.aidl.RemoveByQueryAidlRequest;
import android.app.appsearch.aidl.ReportUsageAidlRequest;
import android.app.appsearch.aidl.SearchAidlRequest;
import android.app.appsearch.aidl.SearchSuggestionAidlRequest;
import android.app.appsearch.aidl.SetSchemaAidlRequest;
import android.app.appsearch.aidl.UnregisterObserverCallbackAidlRequest;
import android.app.appsearch.aidl.WriteSearchResultsToFileAidlRequest;
import android.app.appsearch.exceptions.AppSearchException;
import android.app.appsearch.functions.AppFunctionService;
import android.app.appsearch.functions.ExecuteAppFunctionRequest;
import android.app.appsearch.functions.SafeOneTimeAppSearchResultCallback;
import android.app.appsearch.functions.ServiceCallHelper;
import android.app.appsearch.functions.ServiceCallHelper.ServiceUsageCompleteListener;
import android.app.appsearch.functions.ServiceCallHelperImpl;
import android.app.appsearch.safeparcel.GenericDocumentParcel;
import android.app.appsearch.stats.SchemaMigrationStats;
import android.app.appsearch.util.ExceptionUtil;
import android.app.appsearch.util.LogUtil;
import android.app.role.RoleManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageStats;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.Binder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalManagerRegistry;
import com.android.server.SystemService;
import com.android.server.appsearch.external.localstorage.stats.CallStats;
import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
import com.android.server.appsearch.external.localstorage.stats.SearchStats;
import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats;
import com.android.server.appsearch.external.localstorage.usagereporting.SearchSessionStatsExtractor;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
import com.android.server.appsearch.observer.AppSearchObserverProxy;
import com.android.server.appsearch.stats.StatsCollector;
import com.android.server.appsearch.transformer.EnterpriseSearchResultPageTransformer;
import com.android.server.appsearch.transformer.EnterpriseSearchSpecTransformer;
import com.android.server.appsearch.util.AdbDumpUtil;
import com.android.server.appsearch.util.ApiCallRecord;
import com.android.server.appsearch.util.ExecutorManager;
import com.android.server.appsearch.util.PackageManagerUtil;
import com.android.server.appsearch.util.ServiceImplHelper;
import com.android.server.appsearch.visibilitystore.FrameworkCallerAccess;
import com.android.server.usage.StorageStatsManagerLocal;
import com.android.server.usage.StorageStatsManagerLocal.StorageStatsAugmenter;
import com.google.android.icing.proto.DebugInfoProto;
import com.google.android.icing.proto.DebugInfoVerbosity;
import com.google.android.icing.proto.PersistType;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* The main service implementation which contains AppSearch's platform functionality.
*
* @hide
*/
public class AppSearchManagerService extends SystemService {
private static final String TAG = "AppSearchManagerService";
@VisibleForTesting
static final String SYSTEM_UI_INTELLIGENCE = "android.app.role.SYSTEM_UI_INTELLIGENCE";
/**
* An executor for system activity not tied to any particular user.
*
*
NOTE: Never call shutdownNow(). AppSearchManagerService persists forever even as
* individual users are added and removed -- without this pool the service will be broken. And,
* clients waiting for callbacks will never receive anything and will hang.
*/
private static final Executor SHARED_EXECUTOR = ExecutorManager.createDefaultExecutorService();
private final Context mContext;
private final ExecutorManager mExecutorManager;
private final AppSearchEnvironment mAppSearchEnvironment;
private final ServiceAppSearchConfig mAppSearchConfig;
private PackageManager mPackageManager;
private RoleManager mRoleManager;
private ServiceImplHelper mServiceImplHelper;
private AppSearchUserInstanceManager mAppSearchUserInstanceManager;
// Keep a reference for the lifecycle instance, so we can access other services like
// ContactsIndexer for dumpsys purpose.
private final AppSearchModule.Lifecycle mLifecycle;
private final ServiceCallHelper mAppFunctionServiceCallHelper;
private final SearchSessionStatsExtractor mSearchSessionStatsExtractor;
public AppSearchManagerService(Context context, AppSearchModule.Lifecycle lifecycle) {
this(context, lifecycle, new ServiceCallHelperImpl<>(
context, IAppFunctionService.Stub::asInterface, SHARED_EXECUTOR));
}
@VisibleForTesting
public AppSearchManagerService(
Context context,
AppSearchModule.Lifecycle lifecycle,
ServiceCallHelper appFunctionServiceCallHelper) {
super(context);
mContext = Objects.requireNonNull(context);
mLifecycle = Objects.requireNonNull(lifecycle);
mAppSearchEnvironment = AppSearchEnvironmentFactory.getEnvironmentInstance();
mAppSearchConfig = AppSearchComponentFactory.getConfigInstance(SHARED_EXECUTOR);
mExecutorManager = new ExecutorManager(mAppSearchConfig);
mAppFunctionServiceCallHelper = Objects.requireNonNull(appFunctionServiceCallHelper);
mSearchSessionStatsExtractor = new SearchSessionStatsExtractor();
}
@Override
public void onStart() {
publishBinderService(Context.APP_SEARCH_SERVICE, new Stub());
mPackageManager = getContext().getPackageManager();
mRoleManager = getContext().getSystemService(RoleManager.class);
mServiceImplHelper = new ServiceImplHelper(mContext, mExecutorManager);
mAppSearchUserInstanceManager = AppSearchUserInstanceManager.getInstance();
registerReceivers();
LocalManagerRegistry.getManager(StorageStatsManagerLocal.class)
.registerStorageStatsAugmenter(new AppSearchStorageStatsAugmenter(), TAG);
LocalManagerRegistry.addManager(LocalService.class, new LocalService());
}
@Override
public void onBootPhase(/* @BootPhase */ int phase) {
if (phase == PHASE_BOOT_COMPLETED) {
StatsCollector.getInstance(mContext, SHARED_EXECUTOR);
}
}
private void registerReceivers() {
mContext.registerReceiverForAllUsers(
new UserActionReceiver(),
new IntentFilter(Intent.ACTION_USER_REMOVED),
/* broadcastPermission= */ null,
/* scheduler= */ null);
//TODO(b/145759910) Add a direct callback when user clears the data instead of relying on
// broadcasts
IntentFilter packageChangedFilter = new IntentFilter();
packageChangedFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
packageChangedFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
packageChangedFilter.addDataScheme("package");
packageChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiverForAllUsers(
new PackageChangedReceiver(),
packageChangedFilter,
/* broadcastPermission= */ null,
/* scheduler= */ null);
}
private class UserActionReceiver extends BroadcastReceiver {
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
Objects.requireNonNull(context);
Objects.requireNonNull(intent);
if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
if (userHandle == null) {
Log.e(TAG,
"Extra " + Intent.EXTRA_USER + " is missing in the intent: " + intent);
return;
}
// We can handle user removal the same way as user stopping: shut down the executor
// and close icing. The data of AppSearch is saved in the "credential encrypted"
// system directory of each user. That directory will be auto-deleted when a user is
// removed, so we don't need it handle it specially.
onUserStopping(userHandle);
} else {
Log.e(TAG, "Received unknown intent: " + intent);
}
}
}
private class PackageChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
Objects.requireNonNull(context);
Objects.requireNonNull(intent);
String action = intent.getAction();
if (action == null) {
return;
}
switch (action) {
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
case Intent.ACTION_PACKAGE_DATA_CLEARED:
Uri data = intent.getData();
if (data == null) {
Log.e(TAG, "Data is missing in the intent: " + intent);
return;
}
String packageName = data.getSchemeSpecificPart();
if (packageName == null) {
Log.e(TAG, "Package name is missing in the intent: " + intent);
return;
}
if (LogUtil.DEBUG) {
Log.d(TAG, "Received " + action + " broadcast on package: " + packageName);
}
int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
if (uid == INVALID_UID) {
Log.e(TAG, "uid is missing in the intent: " + intent);
return;
}
handlePackageRemoved(packageName, uid);
break;
default:
Log.e(TAG, "Received unknown intent: " + intent);
}
}
}
private void handlePackageRemoved(@NonNull String packageName, int uid) {
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
if (mServiceImplHelper.isUserLocked(userHandle)) {
// We cannot access a locked user's directory and remove package data from it.
// We should remove those uninstalled package data when the user is unlocking.
return;
}
// Only clear the package's data if AppSearch exists for this user.
if (mAppSearchEnvironment.getAppSearchDir(mContext, userHandle).exists()) {
mExecutorManager.getOrCreateUserExecutor(userHandle).execute(() -> {
try {
Context userContext = mAppSearchEnvironment
.createContextAsUser(mContext, userHandle);
AppSearchUserInstance instance =
mAppSearchUserInstanceManager.getOrCreateUserInstance(
userContext,
userHandle,
mAppSearchConfig);
instance.getAppSearchImpl().clearPackageData(packageName);
dispatchChangeNotifications(instance);
instance.getLogger().removeCacheForPackage(packageName);
} catch (AppSearchException | RuntimeException e) {
Log.e(TAG, "Unable to remove data for package: " + packageName, e);
ExceptionUtil.handleException(e);
}
});
}
}
@Override
public void onUserUnlocking(@NonNull TargetUser user) {
Objects.requireNonNull(user);
UserHandle userHandle = user.getUserHandle();
mServiceImplHelper.setUserIsLocked(userHandle, false);
// Only schedule task if AppSearch exists for this user.
if (mAppSearchEnvironment.getAppSearchDir(mContext, userHandle).exists()) {
mExecutorManager.getOrCreateUserExecutor(userHandle).execute(() -> {
// Try to prune garbage package data, this is to recover if user remove a package
// and reboot the device before we prune the package data.
try {
Context userContext = mAppSearchEnvironment
.createContextAsUser(mContext, userHandle);
AppSearchUserInstance instance =
mAppSearchUserInstanceManager.getOrCreateUserInstance(
userContext,
userHandle,
mAppSearchConfig);
List installedPackageInfos = userContext
.getPackageManager()
.getInstalledPackages(/* flags= */ 0);
Set packagesToKeep = new ArraySet<>(installedPackageInfos.size());
for (int i = 0; i < installedPackageInfos.size(); i++) {
packagesToKeep.add(installedPackageInfos.get(i).packageName);
}
packagesToKeep.add(VisibilityStore.VISIBILITY_PACKAGE_NAME);
instance.getAppSearchImpl().prunePackageData(packagesToKeep);
} catch (AppSearchException | RuntimeException e) {
Log.e(TAG, "Unable to prune packages for " + user, e);
ExceptionUtil.handleException(e);
}
// Try to schedule fully persist job.
try {
AppSearchMaintenanceService.scheduleFullyPersistJob(mContext,
userHandle.getIdentifier(),
mAppSearchConfig.getCachedFullyPersistJobIntervalMillis());
} catch (RuntimeException e) {
Log.e(TAG, "Unable to schedule fully persist job for " + user, e);
ExceptionUtil.handleException(e);
}
});
}
}
@Override
public void onUserStopping(@NonNull TargetUser user) {
Objects.requireNonNull(user);
onUserStopping(user.getUserHandle());
}
private void onUserStopping(@NonNull UserHandle userHandle) {
Objects.requireNonNull(userHandle);
if (LogUtil.INFO) {
Log.i(TAG, "Shutting down AppSearch for user " + userHandle);
}
try {
mServiceImplHelper.setUserIsLocked(userHandle, true);
mExecutorManager.shutDownAndRemoveUserExecutor(userHandle);
mAppSearchUserInstanceManager.closeAndRemoveUserInstance(userHandle);
AppSearchMaintenanceService.cancelFullyPersistJobIfScheduled(
mContext, userHandle.getIdentifier());
if (LogUtil.INFO) {
Log.i(TAG, "Removed AppSearchImpl instance for: " + userHandle);
}
} catch (InterruptedException | RuntimeException e) {
Log.e(TAG, "Unable to remove data for: " + userHandle, e);
ExceptionUtil.handleException(e);
}
}
class LocalService {
/** Persist all pending mutation operation to disk for the given user. */
public void doFullyPersistForUser(@UserIdInt int userId) throws AppSearchException {
UserHandle targetUser = UserHandle.getUserHandleForUid(userId);
AppSearchUserInstance instance =
mAppSearchUserInstanceManager.getUserInstance(targetUser);
instance.getAppSearchImpl().persistToDisk(PersistType.Code.FULL);
}
}
private class Stub extends IAppSearchManager.Stub {
@Override
public void setSchema(
@NonNull SetSchemaAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
long verifyIncomingCallLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_SET_SCHEMA, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
long verifyIncomingCallLatencyEndTimeMillis = SystemClock.elapsedRealtime();
long waitExecutorStartTimeMillis = SystemClock.elapsedRealtime();
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(
targetUser, callback, callingPackageName, CallStats.CALL_TYPE_SET_SCHEMA,
() -> {
long waitExecutorEndTimeMillis = SystemClock.elapsedRealtime();
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
SetSchemaStats.Builder setSchemaStatsBuilder = new SetSchemaStats.Builder(
callingPackageName, request.getDatabaseName());
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
InternalSetSchemaResponse internalSetSchemaResponse =
instance.getAppSearchImpl().setSchema(
callingPackageName,
request.getDatabaseName(),
request.getSchemas(),
request.getVisibilityConfigs(),
request.isForceOverride(),
request.getSchemaVersion(),
setSchemaStatsBuilder);
++operationSuccessCount;
invokeCallbackOnResult(callback, AppSearchResultParcel
.fromInternalSetSchemaResponse(internalSetSchemaResponse));
// Schedule a task to dispatch change notifications. See requirements for where
// the method is called documented in the method description.
long dispatchNotificationLatencyStartTimeMillis = SystemClock.elapsedRealtime();
dispatchChangeNotifications(instance);
long dispatchNotificationLatencyEndTimeMillis = SystemClock.elapsedRealtime();
// setSchema will sync the schemas in the request to AppSearch, any existing
// schemas which are not included in the request will be deleted if we force
// override incompatible schemas. And all documents of these types will be
// deleted as well. We should checkForOptimize for these deletion.
long checkForOptimizeLatencyStartTimeMillis = SystemClock.elapsedRealtime();
checkForOptimize(targetUser, instance);
long checkForOptimizeLatencyEndTimeMillis = SystemClock.elapsedRealtime();
setSchemaStatsBuilder
.setVerifyIncomingCallLatencyMillis(
(int) (verifyIncomingCallLatencyEndTimeMillis
- verifyIncomingCallLatencyStartTimeMillis))
.setExecutorAcquisitionLatencyMillis(
(int) (waitExecutorEndTimeMillis
- waitExecutorStartTimeMillis))
// This operation no longer exists, so this latency is always 0
.setRebuildFromBundleLatencyMillis(0)
.setDispatchChangeNotificationsLatencyMillis(
(int) (dispatchNotificationLatencyEndTimeMillis
- dispatchNotificationLatencyStartTimeMillis))
.setOptimizeLatencyMillis(
(int) (checkForOptimizeLatencyEndTimeMillis
- checkForOptimizeLatencyStartTimeMillis));
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis -
request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_SET_SCHEMA)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
instance.getLogger().logStats(setSchemaStatsBuilder
.setStatusCode(statusCode)
.setSchemaMigrationCallType(request.getSchemaMigrationCallType())
.setTotalLatencyMillis(totalLatencyMillis)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_SET_SCHEMA, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/*numOperations=*/ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void getSchema(
@NonNull GetSchemaAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
// Get the enterprise user for enterprise calls
UserHandle userToQuery = mServiceImplHelper.getUserToQuery(request.isForEnterprise(),
targetUser);
if (userToQuery == null) {
// Return an empty response if we tried to and couldn't get the enterprise user
invokeCallbackOnResult(callback, AppSearchResultParcel.fromGetSchemaResponse(
new GetSchemaResponse.Builder().build()));
return;
}
boolean global = isGlobalCall(callingPackageName, request.getTargetPackageName(),
request.isForEnterprise());
// We deny based on the calling package and calling database names. If the calling
// package does not match the target package, then the call is global and the target
// database is not a calling database.
String callingDatabaseName = global ? null : request.getDatabaseName();
int callType = global ? CallStats.CALL_TYPE_GLOBAL_GET_SCHEMA
: CallStats.CALL_TYPE_GET_SCHEMA;
if (checkCallDenied(callingPackageName, callingDatabaseName, callType, callback,
targetUser, request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, callType, () -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(userToQuery);
boolean callerHasSystemAccess = instance.getVisibilityChecker()
.doesCallerHaveSystemAccess(callingPackageName);
GetSchemaResponse response =
instance.getAppSearchImpl().getSchema(
request.getTargetPackageName(),
request.getDatabaseName(),
new FrameworkCallerAccess(request.getCallerAttributionSource(),
callerHasSystemAccess, request.isForEnterprise()));
++operationSuccessCount;
invokeCallbackOnResult(callback, AppSearchResultParcel
.fromGetSchemaResponse(response)
);
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(callType)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, callingDatabaseName,
callType, targetUser, request.getBinderCallStartTimeMillis(),
totalLatencyStartTimeMillis, /*numOperations=*/ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void getNamespaces(
@NonNull GetNamespacesAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_GET_NAMESPACES, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_GET_NAMESPACES, () -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
List namespaces =
instance.getAppSearchImpl().getNamespaces(
callingPackageName, request.getDatabaseName());
++operationSuccessCount;
invokeCallbackOnResult(callback, AppSearchResultParcel
.fromStringList(namespaces)
);
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_GET_NAMESPACES)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_GET_NAMESPACES, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/*numOperations=*/ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void putDocuments(
@NonNull PutDocumentsAidlRequest request,
@NonNull IAppSearchBatchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_PUT_DOCUMENTS, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ request.getDocumentsParcel().getTotalDocumentCount())) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_PUT_DOCUMENTS, () -> {
@AppSearchResult.ResultCode int statusCode = RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
List takenActionGenericDocuments = null; // initialize later
try {
AppSearchBatchResult.Builder resultBuilder =
new AppSearchBatchResult.Builder<>();
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
List documentParcels =
request.getDocumentsParcel().getDocumentParcels();
List takenActionDocumentParcels =
request.getDocumentsParcel().getTakenActionGenericDocumentParcels();
// Write GenericDocument of general documents
for (int i = 0; i < documentParcels.size(); i++) {
GenericDocument document = new GenericDocument(documentParcels.get(i));
try {
instance.getAppSearchImpl().putDocument(
callingPackageName,
request.getDatabaseName(),
document,
/* sendChangeNotifications= */ true,
instance.getLogger());
resultBuilder.setSuccess(document.getId(), /* value= */ null);
++operationSuccessCount;
} catch (AppSearchException | RuntimeException e) {
// We don't rethrow here, so we can keep trying with the
// following documents.
AppSearchResult result = throwableToFailedResult(e);
resultBuilder.setResult(document.getId(), result);
// Since we can only include one status code in the atom,
// for failures, we would just save the one for the last failure
statusCode = result.getResultCode();
++operationFailureCount;
}
}
// Write GenericDocument of taken actions
if (!takenActionDocumentParcels.isEmpty()) {
takenActionGenericDocuments =
new ArrayList<>(takenActionDocumentParcels.size());
}
for (int i = 0; i < takenActionDocumentParcels.size(); i++) {
GenericDocument document =
new GenericDocument(takenActionDocumentParcels.get(i));
takenActionGenericDocuments.add(document);
try {
instance.getAppSearchImpl().putDocument(
callingPackageName,
request.getDatabaseName(),
document,
/* sendChangeNotifications= */ true,
instance.getLogger());
resultBuilder.setSuccess(document.getId(), /* value= */ null);
++operationSuccessCount;
} catch (AppSearchException | RuntimeException e) {
// We don't rethrow here, so we can keep trying with the
// following documents.
AppSearchResult result = throwableToFailedResult(e);
resultBuilder.setResult(document.getId(), result);
// Since we can only include one status code in the atom,
// for failures, we would just save the one for the last failure
statusCode = result.getResultCode();
++operationFailureCount;
}
}
// Now that the batch has been written. Persist the newly written data.
instance.getAppSearchImpl().persistToDisk(PersistType.Code.LITE);
invokeCallbackOnResult(callback, AppSearchBatchResultParcel
.fromStringToVoid(resultBuilder.build()));
// Schedule a task to dispatch change notifications. See requirements for where
// the method is called documented in the method description.
dispatchChangeNotifications(instance);
// The existing documents with same ID will be deleted, so there may be some
// resources that could be released after optimize().
checkForOptimize(
targetUser,
instance,
/* mutateBatchSize= */
request.getDocumentsParcel().getTotalDocumentCount());
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnError(callback, failedResult);
} finally {
// TODO(b/261959320) add outstanding latency fields in AppSearch stats
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_PUT_DOCUMENTS)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
// Extract metrics from taken action generic documents and add log.
if (takenActionGenericDocuments != null
&& !takenActionGenericDocuments.isEmpty()) {
instance.getLogger()
.logStats(mSearchSessionStatsExtractor.extract(
callingPackageName,
request.getDatabaseName(),
takenActionGenericDocuments));
}
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_PUT_DOCUMENTS, targetUser,
request.getBinderCallStartTimeMillis(),
totalLatencyStartTimeMillis,
/* numOperations= */
request.getDocumentsParcel().getTotalDocumentCount(), RESULT_RATE_LIMITED);
}
}
@Override
public void getDocuments(
@NonNull GetDocumentsAidlRequest request,
@NonNull IAppSearchBatchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
// Get the enterprise user for enterprise calls
UserHandle userToQuery = mServiceImplHelper.getUserToQuery(
request.isForEnterprise(), targetUser);
if (userToQuery == null) {
// Return an empty batch result if we tried to and couldn't get the enterprise user
invokeCallbackOnResult(callback, AppSearchBatchResultParcel
.fromStringToGenericDocumentParcel(new AppSearchBatchResult
.Builder().build()));
return;
}
// TODO(b/319315074): consider removing local getDocument and just use globalGetDocument
// instead; this would simplify the code and assure us that enterprise calls definitely
// go through visibility checks
boolean global = isGlobalCall(callingPackageName, request.getTargetPackageName(),
request.isForEnterprise());
// We deny based on the calling package and calling database names. If the calling
// package does not match the target package, then the call is global and the target
// database is not a calling database.
String callingDatabaseName = global ? null : request.getDatabaseName();
int callType = global ? CallStats.CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID
: CallStats.CALL_TYPE_GET_DOCUMENTS;
if (checkCallDenied(callingPackageName, callingDatabaseName, callType, callback,
targetUser, request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ request.getGetByDocumentIdRequest().getIds().size())) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, callType, () -> {
@AppSearchResult.ResultCode int statusCode = RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
AppSearchBatchResult.Builder resultBuilder =
new AppSearchBatchResult.Builder<>();
instance = mAppSearchUserInstanceManager.getUserInstance(userToQuery);
for (String id : request.getGetByDocumentIdRequest().getIds()) {
try {
GenericDocument document;
if (global) {
boolean callerHasSystemAccess = instance.getVisibilityChecker()
.doesCallerHaveSystemAccess(
request.getCallerAttributionSource()
.getPackageName());
Map> typePropertyPaths =
request.getGetByDocumentIdRequest().getProjections();
if (request.isForEnterprise()) {
EnterpriseSearchSpecTransformer.transformPropertiesMap(
typePropertyPaths);
}
document = instance.getAppSearchImpl().globalGetDocument(
request.getTargetPackageName(),
request.getDatabaseName(),
request.getGetByDocumentIdRequest().getNamespace(),
id,
typePropertyPaths,
new FrameworkCallerAccess(
request.getCallerAttributionSource(),
callerHasSystemAccess,
request.isForEnterprise()));
if (request.isForEnterprise()) {
document =
EnterpriseSearchResultPageTransformer.transformDocument(
request.getTargetPackageName(),
request.getDatabaseName(),
document);
}
} else {
document = instance.getAppSearchImpl().getDocument(
request.getTargetPackageName(),
request.getDatabaseName(),
request.getGetByDocumentIdRequest().getNamespace(),
id,
request.getGetByDocumentIdRequest().getProjections());
}
++operationSuccessCount;
resultBuilder.setSuccess(id, document.getDocumentParcel());
} catch (AppSearchException | RuntimeException e) {
// Since we can only include one status code in the atom,
// for failures, we would just save the one for the last failure
// Also, we don't rethrow here, so we can keep trying for
// the following ones.
AppSearchResult result =
throwableToFailedResult(e);
resultBuilder.setResult(id, result);
statusCode = result.getResultCode();
++operationFailureCount;
}
}
invokeCallbackOnResult(callback, AppSearchBatchResultParcel
.fromStringToGenericDocumentParcel(resultBuilder.build()));
} catch (RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnError(callback, failedResult);
} finally {
// TODO(b/261959320) add outstanding latency fields in AppSearch stats
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis -
request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(callType)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, callingDatabaseName,
callType, targetUser, request.getBinderCallStartTimeMillis(),
totalLatencyStartTimeMillis,
/* numOperations= */ request.getGetByDocumentIdRequest().getIds().size(),
RESULT_RATE_LIMITED);
}
}
@Override
public void search(
@NonNull SearchAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_SEARCH, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_SEARCH, () -> {
@AppSearchResult.ResultCode int statusCode = RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
SearchResultPage searchResultPage = instance.getAppSearchImpl().query(
callingPackageName,
request.getDatabaseName(),
request.getSearchExpression(),
request.getSearchSpec(),
instance.getLogger());
++operationSuccessCount;
invokeCallbackOnResult(
callback,
AppSearchResultParcel.fromSearchResultPage(searchResultPage));
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis = 2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_SEARCH)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_SEARCH, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void globalSearch(
@NonNull GlobalSearchAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
// Get the enterprise user for enterprise calls
UserHandle userToQuery = mServiceImplHelper.getUserToQuery(request.isForEnterprise(),
targetUser);
if (userToQuery == null) {
// Return an empty result if we tried to and couldn't get the enterprise user
invokeCallbackOnResult(callback,
AppSearchResultParcel.fromSearchResultPage(new SearchResultPage()));
return;
}
if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
CallStats.CALL_TYPE_GLOBAL_SEARCH, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_GLOBAL_SEARCH, () -> {
@AppSearchResult.ResultCode int statusCode = RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(userToQuery);
boolean callerHasSystemAccess = instance.getVisibilityChecker()
.doesCallerHaveSystemAccess(callingPackageName);
SearchSpec querySearchSpec = request.isForEnterprise()
? EnterpriseSearchSpecTransformer.transformSearchSpec(
request.getSearchSpec()) : request.getSearchSpec();
SearchResultPage searchResultPage = instance.getAppSearchImpl().globalQuery(
request.getSearchExpression(),
querySearchSpec,
new FrameworkCallerAccess(request.getCallerAttributionSource(),
callerHasSystemAccess, request.isForEnterprise()),
instance.getLogger());
if (request.isForEnterprise()) {
searchResultPage =
EnterpriseSearchResultPageTransformer.transformSearchResultPage(
searchResultPage);
}
++operationSuccessCount;
invokeCallbackOnResult(
callback,
AppSearchResultParcel.fromSearchResultPage(searchResultPage));
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis = 2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_GLOBAL_SEARCH)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName,
/* callingDatabaseName= */ null, CallStats.CALL_TYPE_GLOBAL_SEARCH,
targetUser, request.getBinderCallStartTimeMillis(),
totalLatencyStartTimeMillis, /* numOperations= */ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void getNextPage(
@NonNull GetNextPageAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
// Get the enterprise user for enterprise calls
UserHandle userToQuery = mServiceImplHelper.getUserToQuery(request.isForEnterprise(),
targetUser);
if (userToQuery == null) {
// Return an empty result if we tried to and couldn't get the enterprise user
invokeCallbackOnResult(callback,
AppSearchResultParcel.fromSearchResultPage(new SearchResultPage()));
return;
}
// Enterprise session calls are considered global for CallStats logging
boolean global = request.getDatabaseName() == null || request.isForEnterprise();
int callType = global ? CallStats.CALL_TYPE_GLOBAL_GET_NEXT_PAGE
: CallStats.CALL_TYPE_GET_NEXT_PAGE;
if (checkCallDenied(callingPackageName, request.getDatabaseName(), callType, callback,
targetUser, request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, callType, () -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
SearchStats.Builder statsBuilder;
if (global) {
statsBuilder = new SearchStats.Builder(VISIBILITY_SCOPE_GLOBAL,
callingPackageName)
.setJoinType(request.getJoinType());
} else {
statsBuilder = new SearchStats.Builder(VISIBILITY_SCOPE_LOCAL,
callingPackageName)
.setDatabase(request.getDatabaseName())
.setJoinType(request.getJoinType());
}
try {
instance = mAppSearchUserInstanceManager.getUserInstance(userToQuery);
SearchResultPage searchResultPage =
instance.getAppSearchImpl().getNextPage(callingPackageName,
request.getNextPageToken(), statsBuilder);
if (request.isForEnterprise()) {
searchResultPage =
EnterpriseSearchResultPageTransformer.transformSearchResultPage(
searchResultPage);
}
++operationSuccessCount;
invokeCallbackOnResult(
callback,
AppSearchResultParcel.fromSearchResultPage(searchResultPage));
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis = 2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
CallStats.Builder builder = new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(callType)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount);
instance.getLogger().logStats(builder.build());
instance.getLogger().logStats(statsBuilder.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
callType, targetUser, request.getBinderCallStartTimeMillis(),
totalLatencyStartTimeMillis, /* numOperations= */ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void invalidateNextPageToken(@NonNull InvalidateNextPageTokenAidlRequest request) {
Objects.requireNonNull(request);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
try {
UserHandle targetUser = mServiceImplHelper.verifyIncomingCall(
request.getCallerAttributionSource(), request.getUserHandle());
// Get the enterprise user for enterprise calls
UserHandle userToQuery = mServiceImplHelper.getUserToQuery(
request.isForEnterprise(), targetUser);
if (userToQuery == null) {
// Return if we tried to and couldn't get the enterprise user
return;
}
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserNoCallbackAsync(
targetUser, callingPackageName,
CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN, () -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(userToQuery);
instance.getAppSearchImpl().invalidateNextPageToken(
callingPackageName, request.getNextPageToken());
operationSuccessCount++;
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
statusCode = throwableToFailedResult(e).getResultCode();
Log.e(TAG, "Unable to invalidate the query page token", e);
ExceptionUtil.handleException(e);
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime()
- totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN)
// TODO(b/173532925) check the existing binder call latency
// chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(
callingPackageName, /* callingDatabaseName= */ null,
CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1, RESULT_RATE_LIMITED);
}
} catch (RuntimeException e) {
Log.e(TAG, "Unable to invalidate the query page token", e);
ExceptionUtil.handleException(e);
}
}
@Override
public void writeSearchResultsToFile(
@NonNull WriteSearchResultsToFileAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE,
() -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
// we don't need to append the file. The file is always brand new.
try (DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(
request.getParcelFileDescriptor().getFileDescriptor()))) {
SearchResultPage searchResultPage = instance.getAppSearchImpl().query(
callingPackageName,
request.getDatabaseName(),
request.getSearchExpression(),
request.getSearchSpec(),
/* logger= */ null);
while (!searchResultPage.getResults().isEmpty()) {
for (int i = 0; i < searchResultPage.getResults().size(); i++) {
AppSearchMigrationHelper.writeDocumentToOutputStream(
outputStream,
searchResultPage.getResults().get(i).getGenericDocument());
}
operationSuccessCount += searchResultPage.getResults().size();
// TODO(b/173532925): Implement logging for statsBuilder
searchResultPage = instance.getAppSearchImpl().getNextPage(
callingPackageName,
searchResultPage.getNextPageToken(),
/* sStatsBuilder= */ null);
}
}
invokeCallbackOnResult(callback, AppSearchResultParcel.fromVoid());
} catch (AppSearchException | IOException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis = 2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void putDocumentsFromFile(
@NonNull PutDocumentsFromFileAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long callStatsTotalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
// Since we don't read from the given file, we don't know the number of documents so we
// just set numOperations to 1 instead
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_PUT_DOCUMENTS_FROM_FILE, callback, targetUser,
request.getBinderCallStartTimeMillis(), callStatsTotalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_PUT_DOCUMENTS_FROM_FILE,
() -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
SchemaMigrationStats.Builder schemaMigrationStatsBuilder = new SchemaMigrationStats
.Builder(request.getSchemaMigrationStats());
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
GenericDocument document;
ArrayList migrationFailures = new ArrayList<>();
try (DataInputStream inputStream = new DataInputStream(new FileInputStream(
request.getParcelFileDescriptor().getFileDescriptor()))) {
while (true) {
try {
document = AppSearchMigrationHelper
.readDocumentFromInputStream(inputStream);
} catch (EOFException e) {
// nothing wrong, we just finish the reading.
break;
}
try {
// Per this method's documentation, individual document change
// notifications are not dispatched.
instance.getAppSearchImpl().putDocument(
callingPackageName,
request.getDatabaseName(),
document,
/* sendChangeNotifications= */ false,
/* logger= */ null);
++operationSuccessCount;
} catch (AppSearchException | RuntimeException e) {
// We don't rethrow here, so we can still keep going with the
// following documents.
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
migrationFailures.add(new SetSchemaResponse.MigrationFailure(
document.getNamespace(),
document.getId(),
document.getSchemaType(),
failedResult));
}
}
}
instance.getAppSearchImpl().persistToDisk(PersistType.Code.FULL);
schemaMigrationStatsBuilder
.setTotalSuccessMigratedDocumentCount(operationSuccessCount)
.setMigrationFailureCount(migrationFailures.size());
invokeCallbackOnResult(callback, AppSearchResultParcel
.fromMigrationFailuresList(migrationFailures));
} catch (AppSearchException | IOException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
long latencyEndTimeMillis =
SystemClock.elapsedRealtime();
int estimatedBinderLatencyMillis =
2 * (int) (callStatsTotalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int callStatsTotalLatencyMillis =
(int) (latencyEndTimeMillis - callStatsTotalLatencyStartTimeMillis);
// totalLatencyStartTimeMillis is captured in the SDK side, and
// put migrate documents is the last step of migration process.
// This should includes whole schema migration process.
// Like get old schema, first and second set schema, query old
// documents, transform documents and save migrated documents.
int totalLatencyMillis = (int) (latencyEndTimeMillis
- request.getTotalLatencyStartTimeMillis());
int saveDocumentLatencyMillis = (int) (latencyEndTimeMillis
- request.getBinderCallStartTimeMillis());
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(callStatsTotalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_PUT_DOCUMENTS_FROM_FILE)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
instance.getLogger().logStats(schemaMigrationStatsBuilder
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setSaveDocumentLatencyMillis(saveDocumentLatencyMillis)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_PUT_DOCUMENTS_FROM_FILE, targetUser,
request.getBinderCallStartTimeMillis(),
callStatsTotalLatencyStartTimeMillis, /* numOperations= */ 1,
RESULT_RATE_LIMITED);
}
}
@Override
public void searchSuggestion(
@NonNull SearchSuggestionAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_SEARCH_SUGGESTION, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_SEARCH_SUGGESTION,
() -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
// TODO(b/173532925): Implement logging for statsBuilder
List searchSuggestionResults =
instance.getAppSearchImpl().searchSuggestion(
callingPackageName,
request.getDatabaseName(),
request.getSuggestionQueryExpression(),
request.getSearchSuggestionSpec());
++operationSuccessCount;
invokeCallbackOnResult(
callback, AppSearchResultParcel
.fromSearchSuggestionResultList(searchSuggestionResults));
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_SEARCH_SUGGESTION)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_SEARCH_SUGGESTION, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void reportUsage(
@NonNull ReportUsageAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
// We deny based on the calling package and calling database names. If the API call is
// intended for system usage, then the call is global, and the target database is not a
// calling database.
String callingDatabaseName = request.isSystemUsage()
? null : request.getDatabaseName();
int callType = request.isSystemUsage() ? CallStats.CALL_TYPE_REPORT_SYSTEM_USAGE
: CallStats.CALL_TYPE_REPORT_USAGE;
if (checkCallDenied(callingPackageName, callingDatabaseName, callType, callback,
targetUser, request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_REPORT_USAGE,
() -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
if (request.isSystemUsage()) {
if (!instance.getVisibilityChecker().doesCallerHaveSystemAccess(
callingPackageName)) {
throw new AppSearchException(RESULT_SECURITY_ERROR,
callingPackageName
+ " does not have access to report system usage");
}
} else {
if (!callingPackageName.equals(request.getTargetPackageName())) {
throw new AppSearchException(RESULT_SECURITY_ERROR,
"Cannot report usage to different package: "
+ request.getTargetPackageName() + " from package: "
+ callingPackageName);
}
}
instance.getAppSearchImpl().reportUsage(request.getTargetPackageName(),
request.getDatabaseName(),
request.getReportUsageRequest().getNamespace(),
request.getReportUsageRequest().getDocumentId(),
request.getReportUsageRequest().getUsageTimestampMillis(),
request.isSystemUsage());
++operationSuccessCount;
invokeCallbackOnResult(callback, AppSearchResultParcel.fromVoid());
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis -
request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(callType)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, callingDatabaseName,
callType, targetUser, request.getBinderCallStartTimeMillis(),
totalLatencyStartTimeMillis,
/* numOperations= */ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void removeByDocumentId(
@NonNull RemoveByDocumentIdAidlRequest request,
@NonNull IAppSearchBatchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ request.getRemoveByDocumentIdRequest().getIds().size())) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID,
() -> {
@AppSearchResult.ResultCode int statusCode = RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
AppSearchBatchResult.Builder resultBuilder =
new AppSearchBatchResult.Builder<>();
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
for (String id : request.getRemoveByDocumentIdRequest().getIds()) {
try {
instance.getAppSearchImpl().remove(
callingPackageName,
request.getDatabaseName(),
request.getRemoveByDocumentIdRequest().getNamespace(),
id,
/* removeStatsBuilder= */ null);
++operationSuccessCount;
resultBuilder.setSuccess(id, /*result= */ null);
} catch (AppSearchException | RuntimeException e) {
// We don't rethrow here, so we can still keep trying for the following
// ones.
AppSearchResult result = throwableToFailedResult(e);
resultBuilder.setResult(id, result);
// Since we can only include one status code in the atom,
// for failures, we would just save the one for the last failure
statusCode = result.getResultCode();
++operationFailureCount;
}
}
// Now that the batch has been written. Persist the newly written data.
instance.getAppSearchImpl().persistToDisk(PersistType.Code.LITE);
invokeCallbackOnResult(callback, AppSearchBatchResultParcel.fromStringToVoid(
resultBuilder.build()));
// Schedule a task to dispatch change notifications. See requirements for where
// the method is called documented in the method description.
dispatchChangeNotifications(instance);
checkForOptimize(targetUser, instance,
request.getRemoveByDocumentIdRequest().getIds().size());
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnError(callback, failedResult);
} finally {
// TODO(b/261959320) add outstanding latency fields in AppSearch stats
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ request.getRemoveByDocumentIdRequest().getIds().size(),
RESULT_RATE_LIMITED);
}
}
@Override
public void removeByQuery(
@NonNull RemoveByQueryAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH,
() -> {
@AppSearchResult.ResultCode int statusCode = RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
instance.getAppSearchImpl().removeByQuery(
callingPackageName,
request.getDatabaseName(),
request.getQueryExpression(),
request.getSearchSpec(),
/* removeStatsBuilder= */ null);
// Now that the batch has been written. Persist the newly written data.
instance.getAppSearchImpl().persistToDisk(PersistType.Code.LITE);
++operationSuccessCount;
invokeCallbackOnResult(callback, AppSearchResultParcel.fromVoid());
// Schedule a task to dispatch change notifications. See requirements for where
// the method is called documented in the method description.
dispatchChangeNotifications(instance);
checkForOptimize(targetUser, instance);
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
// TODO(b/261959320) add outstanding latency fields in AppSearch stats
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void getStorageInfo(
@NonNull GetStorageInfoAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_GET_STORAGE_INFO, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
callback, callingPackageName, CallStats.CALL_TYPE_GET_STORAGE_INFO, () -> {
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
StorageInfo storageInfo = instance.getAppSearchImpl().getStorageInfoForDatabase(
callingPackageName, request.getDatabaseName());
++operationSuccessCount;
invokeCallbackOnResult(
callback, AppSearchResultParcel.fromStorageInfo(storageInfo));
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(request.getDatabaseName())
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_GET_STORAGE_INFO)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
CallStats.CALL_TYPE_GET_STORAGE_INFO, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1, RESULT_RATE_LIMITED);
}
}
@Override
public void persistToDisk(@NonNull PersistToDiskAidlRequest request) {
Objects.requireNonNull(request);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
try {
UserHandle targetUser = mServiceImplHelper.verifyIncomingCall(
request.getCallerAttributionSource(), request.getUserHandle());
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
CallStats.CALL_TYPE_FLUSH, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserNoCallbackAsync(
targetUser, callingPackageName, CallStats.CALL_TYPE_FLUSH, () -> {
@AppSearchResult.ResultCode int statusCode = RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
instance.getAppSearchImpl().persistToDisk(PersistType.Code.FULL);
++operationSuccessCount;
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
statusCode = throwableToFailedResult(e).getResultCode();
// We will print two error messages if we rethrow, but I would rather keep
// this print statement here, so we know where the actual exception
// comes from.
Log.e(TAG, "Unable to persist the data to disk", e);
ExceptionUtil.handleException(e);
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime()
- totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_FLUSH)
// TODO(b/173532925) check the existing binder call latency
// chart is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(
callingPackageName, /* callingDatabaseName= */ null,
CallStats.CALL_TYPE_FLUSH, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1, RESULT_RATE_LIMITED);
}
} catch (RuntimeException e) {
Log.e(TAG, "Unable to persist the data to disk", e);
ExceptionUtil.handleException(e);
}
}
@Override
public AppSearchResultParcel registerObserverCallback(
@NonNull RegisterObserverCallbackAidlRequest request,
@NonNull IAppSearchObserverProxy observerProxyStub) {
Objects.requireNonNull(request);
Objects.requireNonNull(observerProxyStub);
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
String callingPackageName = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
// Note: registerObserverCallback is performed on the binder thread, unlike most
// AppSearch APIs
try {
UserHandle targetUser = mServiceImplHelper.verifyIncomingCall(
request.getCallerAttributionSource(), request.getUserHandle());
callingPackageName = request.getCallerAttributionSource().getPackageName();
if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
CallStats.CALL_TYPE_REGISTER_OBSERVER_CALLBACK, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return AppSearchResultParcel.fromFailedResult(AppSearchResult.newFailedResult(
RESULT_DENIED, null));
}
long callingIdentity = Binder.clearCallingIdentity();
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
// Prepare a new ObserverProxy linked to this binder.
AppSearchObserverProxy observerProxy =
new AppSearchObserverProxy(observerProxyStub);
// Watch for client disconnection, unregistering the observer if it happens.
final AppSearchUserInstance finalInstance = instance;
observerProxyStub.asBinder().linkToDeath(
() -> finalInstance.getAppSearchImpl()
.unregisterObserverCallback(
request.getTargetPackageName(), observerProxy),
/* flags= */ 0);
// Register the observer.
boolean callerHasSystemAccess = instance.getVisibilityChecker()
.doesCallerHaveSystemAccess(callingPackageName);
instance.getAppSearchImpl().registerObserverCallback(
new FrameworkCallerAccess(request.getCallerAttributionSource(),
callerHasSystemAccess, /*isForEnterprise=*/ false),
request.getTargetPackageName(),
request.getObserverSpec(),
mExecutorManager.getOrCreateUserExecutor(targetUser),
new AppSearchObserverProxy(observerProxyStub));
++operationSuccessCount;
return AppSearchResultParcel.fromVoid();
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
} catch (RemoteException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
return AppSearchResultParcel.fromFailedResult(failedResult);
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_REGISTER_OBSERVER_CALLBACK)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
}
@Override
public AppSearchResultParcel unregisterObserverCallback(
@NonNull UnregisterObserverCallbackAidlRequest request,
@NonNull IAppSearchObserverProxy observerProxyStub) {
Objects.requireNonNull(request);
Objects.requireNonNull(observerProxyStub);
@AppSearchResult.ResultCode int statusCode = AppSearchResult.RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
// Note: unregisterObserverCallback is performed on the binder thread, unlike most
// AppSearch APIs
try {
UserHandle targetUser = mServiceImplHelper.verifyIncomingCall(
request.getCallerAttributionSource(), request.getUserHandle());
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
CallStats.CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return AppSearchResultParcel.fromFailedResult(AppSearchResult.newFailedResult(
RESULT_DENIED, null));
}
long callingIdentity = Binder.clearCallingIdentity();
try {
instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
instance.getAppSearchImpl().unregisterObserverCallback(
request.getObservedPackage(),
new AppSearchObserverProxy(observerProxyStub));
++operationSuccessCount;
return AppSearchResultParcel.fromVoid();
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
} catch (RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
return AppSearchResultParcel.fromFailedResult(failedResult);
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
String callingPackageName = request.getCallerAttributionSource()
.getPackageName();
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
}
@Override
public void initialize(
@NonNull InitializeAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
String callingPackageName = request.getCallerAttributionSource().getPackageName();
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (mAppSearchConfig.getCachedDenylist().checkDeniedPackage(callingPackageName,
CallStats.CALL_TYPE_INITIALIZE)) {
// Note: can't log CallStats here since UserInstance isn't guaranteed to (and most
// likely does not) exist
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
AppSearchResult.newFailedResult(RESULT_DENIED, null)));
return;
}
mServiceImplHelper.executeLambdaForUserAsync(targetUser, callback, callingPackageName,
CallStats.CALL_TYPE_INITIALIZE, () -> {
@AppSearchResult.ResultCode int statusCode = RESULT_OK;
AppSearchUserInstance instance = null;
int operationSuccessCount = 0;
int operationFailureCount = 0;
try {
Context targetUserContext = mAppSearchEnvironment
.createContextAsUser(mContext, request.getUserHandle());
instance = mAppSearchUserInstanceManager.getOrCreateUserInstance(
targetUserContext,
targetUser,
mAppSearchConfig);
++operationSuccessCount;
invokeCallbackOnResult(callback, AppSearchResultParcel.fromVoid());
} catch (AppSearchException | RuntimeException e) {
++operationFailureCount;
AppSearchResult failedResult = throwableToFailedResult(e);
statusCode = failedResult.getResultCode();
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
failedResult));
} finally {
if (instance != null) {
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_INITIALIZE)
// TODO(b/173532925) check the existing binder call latency chart
// is good enough for us:
// http://dashboards/view/_72c98f9a_91d9_41d4_ab9a_bc14f79742b4
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsSucceeded(operationSuccessCount)
.setNumOperationsFailed(operationFailureCount)
.build());
}
}
});
}
@Override
public void executeAppFunction(
@NonNull ExecuteAppFunctionAidlRequest request,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(callback);
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
String callingPackageName = request.getCallerAttributionSource().getPackageName();
UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
request.getCallerAttributionSource(), request.getUserHandle(), callback);
if (targetUser == null) {
return; // Verification failed; verifyIncomingCall triggered callback.
}
if (checkCallDenied(
callingPackageName, /* databaseName= */ null,
CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION, callback, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/* numOperations= */ 1)) {
return;
}
// Log the stats as well whenever we invoke the AppSearchResultCallback.
final SafeOneTimeAppSearchResultCallback safeCallback =
new SafeOneTimeAppSearchResultCallback(callback, result -> {
AppSearchUserInstance instance =
mAppSearchUserInstanceManager.getUserInstance(targetUser);
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis
- request.getBinderCallStartTimeMillis());
instance.getLogger().logStats(new CallStats.Builder()
.setPackageName(callingPackageName)
.setStatusCode(result.getResultCode())
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION)
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.build());
});
// TODO(b/327134039): Add a new policy for this in W timeframe.
if (mServiceImplHelper.isUserOrganizationManaged(targetUser)) {
safeCallback.onFailedResult(AppSearchResult.newFailedResult(
RESULT_SECURITY_ERROR,
"Cannot run on a device with a device owner or from the managed profile."));
return;
}
String targetPackageName = request.getClientRequest().getTargetPackageName();
if (TextUtils.isEmpty(targetPackageName)) {
safeCallback.onFailedResult(AppSearchResult.newFailedResult(
RESULT_INVALID_ARGUMENT,
"targetPackageName cannot be empty."));
return;
}
if (!verifyExecuteAppFunctionCaller(
callingPackageName,
targetPackageName,
targetUser)) {
safeCallback.onFailedResult(AppSearchResult.newFailedResult(
RESULT_SECURITY_ERROR,
callingPackageName + " is not allowed to call executeAppFunction"));
return;
}
boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(
targetUser, callback, callingPackageName,
CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION,
() -> executeAppFunctionUnchecked(
request.getClientRequest(),
targetUser,
safeCallback));
if (!callAccepted) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, /* databaseName= */ null,
CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION, targetUser,
request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
/*numOperations=*/ 1, RESULT_RATE_LIMITED);
}
}
/**
* The same as {@link #executeAppFunction}, except this is without the caller check.
* This method runs on the user-local thread pool.
*/
@WorkerThread
private void executeAppFunctionUnchecked(
@NonNull ExecuteAppFunctionRequest request,
@NonNull UserHandle userHandle,
@NonNull SafeOneTimeAppSearchResultCallback safeCallback) {
Intent serviceIntent = new Intent(AppFunctionService.SERVICE_INTERFACE);
serviceIntent.setPackage(request.getTargetPackageName());
Context userContext = mAppSearchEnvironment.createContextAsUser(mContext, userHandle);
ResolveInfo resolveInfo = userContext.getPackageManager()
.resolveService(serviceIntent, 0);
if (resolveInfo == null || resolveInfo.serviceInfo == null) {
safeCallback.onFailedResult(AppSearchResult.newFailedResult(
RESULT_NOT_FOUND, "Cannot find the target service."));
return;
}
ServiceInfo serviceInfo = resolveInfo.serviceInfo;
if (!PERMISSION_BIND_APP_FUNCTION_SERVICE.equals(serviceInfo.permission)) {
safeCallback.onFailedResult(AppSearchResult.newFailedResult(
RESULT_NOT_FOUND,
"Failed to find a valid target service. The resolved service is missing "
+ "the BIND_APP_FUNCTION_SERVICE permission."));
return;
}
serviceIntent.setComponent(
new ComponentName(serviceInfo.packageName, serviceInfo.name));
if (request.getSha256Certificate() != null) {
if (!PackageManagerUtil.hasSigningCertificate(
mContext, request.getTargetPackageName(), request.getSha256Certificate())) {
safeCallback.onFailedResult(
AppSearchResult.newFailedResult(
RESULT_NOT_FOUND, "Cannot find the target service"));
return;
}
}
boolean bindServiceResult = mAppFunctionServiceCallHelper.runServiceCall(
serviceIntent,
Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS | Context.BIND_AUTO_CREATE,
mAppSearchConfig.getAppFunctionCallTimeoutMillis(),
userHandle,
new ServiceCallHelper.RunServiceCallCallback<>() {
@Override
public void onServiceConnected(
@NonNull IAppFunctionService service,
@NonNull ServiceUsageCompleteListener completeListener) {
try {
service.executeAppFunction(
request,
new IAppSearchResultCallback.Stub() {
@Override
public void onResult(
AppSearchResultParcel resultParcel) {
safeCallback.onResult(resultParcel);
completeListener.onCompleted();
}
});
} catch (Exception e) {
safeCallback.onFailedResult(AppSearchResult
.throwableToFailedResult(e));
completeListener.onCompleted();
}
}
@Override
public void onFailedToConnect() {
safeCallback.onFailedResult(
AppSearchResult.newFailedResult(RESULT_INTERNAL_ERROR, null));
}
@Override
public void onTimedOut() {
safeCallback.onFailedResult(
AppSearchResult.newFailedResult(RESULT_TIMED_OUT, null));
}
});
if (!bindServiceResult) {
safeCallback.onFailedResult(AppSearchResult.newFailedResult(
RESULT_INTERNAL_ERROR, "Failed to bind the target service."));
}
}
/**
* Determines whether the caller is authorized to execute an app function via
* {@link #executeAppFunction}.
*
* Authorization is granted under the following conditions:
*
* - The caller is the same app that owns the target function.
* - The caller possesses the SYSTEM_UI_INTELLIGENCE role for the target user.
*
*
* @param callingPackage The validated package name of the calling app.
* @param targetPackage The package name of the target app.
* @param targetUser The target user.
* @return {@code true} if the caller is authorized, {@code false} otherwise.
*/
private boolean verifyExecuteAppFunctionCaller(
@NonNull String callingPackage,
@NonNull String targetPackage,
@NonNull UserHandle targetUser) {
// While adding new system role-based permissions through mainline updates is possible,
// granting them to system apps in previous android versions is not. System apps must
// request permissions in their prebuilt APKs included in the system image. We cannot
// modify prebuilts in older images anymore.
// TODO(b/327134039): Enforce permission checking for Android V+ or W+, depending on
// whether the new prebuilt can be included in the system image on time.
if (callingPackage.equals(targetPackage)) {
return true;
}
long originalToken = Binder.clearCallingIdentity();
try {
List systemUiIntelligencePackages =
mRoleManager.getRoleHoldersAsUser(SYSTEM_UI_INTELLIGENCE, targetUser);
return systemUiIntelligencePackages.contains(callingPackage);
} finally {
Binder.restoreCallingIdentity(originalToken);
}
}
@BinderThread
private void dumpContactsIndexer(@NonNull PrintWriter pw, boolean verbose) {
Objects.requireNonNull(pw);
UserHandle currentUser = UserHandle.getUserHandleForUid(Binder.getCallingUid());
try {
pw.println("ContactsIndexer stats for " + currentUser);
mLifecycle.dumpContactsIndexerForUser(currentUser, pw, verbose);
} catch (Exception e) {
String errorMessage =
"Unable to dump the internal contacts indexer state for the user: "
+ currentUser;
Log.e(TAG, errorMessage, e);
pw.println(errorMessage);
}
}
@BinderThread
private void dumpAppSearch(@NonNull PrintWriter pw, boolean verbose) {
Objects.requireNonNull(pw);
UserHandle currentUser = UserHandle.getUserHandleForUid(Binder.getCallingUid());
try {
AppSearchUserInstance instance = mAppSearchUserInstanceManager.getUserInstance(
currentUser);
// Print out the recorded last called APIs.
List lastCalledApis = instance.getLogger().getLastCalledApis();
if (!lastCalledApis.isEmpty()) {
pw.println("Last Called APIs:");
for (int i = 0; i < lastCalledApis.size(); i++) {
pw.println(lastCalledApis.get(i));
}
pw.println();
}
DebugInfoProto debugInfo = instance.getAppSearchImpl().getRawDebugInfoProto(
verbose ? DebugInfoVerbosity.Code.DETAILED
: DebugInfoVerbosity.Code.BASIC);
// TODO(b/229778472) Consider showing the original names of namespaces and types
// for a specific package if the package name is passed as a parameter from users.
debugInfo = AdbDumpUtil.desensitizeDebugInfo(debugInfo);
pw.println(debugInfo.getIndexInfo().getIndexStorageInfo());
pw.println();
pw.println("lite_index_info:");
pw.println(debugInfo.getIndexInfo().getLiteIndexInfo());
pw.println();
pw.println("main_index_info:");
pw.println(debugInfo.getIndexInfo().getMainIndexInfo());
pw.println();
pw.println(debugInfo.getDocumentInfo());
pw.println();
pw.println(debugInfo.getSchemaInfo());
} catch (Exception e) {
String errorMessage =
"Unable to dump the internal state for the user: " + currentUser;
Log.e(TAG, errorMessage, e);
pw.println(errorMessage);
}
}
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump AppSearchManagerService from pid="
+ Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+ " due to missing android.permission.DUMP permission");
return;
}
boolean verbose = false;
if (args != null) {
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (Objects.equals(arg, "-h")) {
pw.println(
"Dumps the internal state of AppSearch platform storage and "
+ "AppSearch Contacts Indexer for the current user.");
pw.println("-v, verbose mode");
return;
} else if (Objects.equals(arg, "-v") || Objects.equals(arg, "-a")) {
// "-a" is included when adb dumps all services e.g. in adb bugreport so we
// want to run in verbose mode when this happens
verbose = true;
}
}
}
dumpAppSearch(pw, verbose);
dumpContactsIndexer(pw, verbose);
}
}
private class AppSearchStorageStatsAugmenter implements StorageStatsAugmenter {
@Override
public void augmentStatsForPackageForUser(
@NonNull PackageStats stats,
@NonNull String packageName,
@NonNull UserHandle userHandle,
boolean canCallerAccessAllStats) {
Objects.requireNonNull(stats);
Objects.requireNonNull(packageName);
Objects.requireNonNull(userHandle);
try {
mServiceImplHelper.verifyUserUnlocked(userHandle);
AppSearchUserInstance instance =
mAppSearchUserInstanceManager.getUserInstanceOrNull(userHandle);
if (instance == null) {
// augment storage info from file
Context userContext = mAppSearchEnvironment
.createContextAsUser(mContext, userHandle);
UserStorageInfo userStorageInfo =
mAppSearchUserInstanceManager.getOrCreateUserStorageInfoInstance(
userContext, userHandle);
stats.dataSize +=
userStorageInfo.getSizeBytesForPackage(packageName);
} else {
stats.dataSize += instance.getAppSearchImpl()
.getStorageInfoForPackage(packageName).getSizeBytes();
}
} catch (AppSearchException | RuntimeException e) {
Log.e(
TAG,
"Unable to augment storage stats for "
+ userHandle
+ " packageName "
+ packageName,
e);
ExceptionUtil.handleException(e);
}
}
@Override
public void augmentStatsForUid(
@NonNull PackageStats stats, int uid, boolean canCallerAccessAllStats) {
Objects.requireNonNull(stats);
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
try {
mServiceImplHelper.verifyUserUnlocked(userHandle);
String[] packagesForUid = mPackageManager.getPackagesForUid(uid);
if (packagesForUid == null) {
return;
}
AppSearchUserInstance instance =
mAppSearchUserInstanceManager.getUserInstanceOrNull(userHandle);
if (instance == null) {
// augment storage info from file
Context userContext = mAppSearchEnvironment
.createContextAsUser(mContext, userHandle);
UserStorageInfo userStorageInfo =
mAppSearchUserInstanceManager.getOrCreateUserStorageInfoInstance(
userContext, userHandle);
for (int i = 0; i < packagesForUid.length; i++) {
stats.dataSize += userStorageInfo.getSizeBytesForPackage(
packagesForUid[i]);
}
} else {
for (int i = 0; i < packagesForUid.length; i++) {
stats.dataSize += instance.getAppSearchImpl()
.getStorageInfoForPackage(packagesForUid[i]).getSizeBytes();
}
}
} catch (AppSearchException | RuntimeException e) {
Log.e(TAG, "Unable to augment storage stats for uid " + uid, e);
ExceptionUtil.handleException(e);
}
}
@Override
public void augmentStatsForUser(
@NonNull PackageStats stats, @NonNull UserHandle userHandle) {
// TODO(b/179160886): this implementation could incur many jni calls and a lot of
// in-memory processing from getStorageInfoForPackage. Instead, we can just compute the
// size of the icing dir (or use the overall StorageInfo without interpolating it).
Objects.requireNonNull(stats);
Objects.requireNonNull(userHandle);
try {
mServiceImplHelper.verifyUserUnlocked(userHandle);
AppSearchUserInstance instance =
mAppSearchUserInstanceManager.getUserInstanceOrNull(userHandle);
if (instance == null) {
// augment storage info from file
Context userContext = mAppSearchEnvironment
.createContextAsUser(mContext, userHandle);
UserStorageInfo userStorageInfo =
mAppSearchUserInstanceManager.getOrCreateUserStorageInfoInstance(
userContext, userHandle);
stats.dataSize += userStorageInfo.getTotalSizeBytes();
} else {
List packagesForUser = mPackageManager.getInstalledPackagesAsUser(
/* flags= */ 0, userHandle.getIdentifier());
if (packagesForUser != null) {
for (int i = 0; i < packagesForUser.size(); i++) {
String packageName = packagesForUser.get(i).packageName;
stats.dataSize += instance.getAppSearchImpl()
.getStorageInfoForPackage(packageName).getSizeBytes();
}
}
}
} catch (AppSearchException | RuntimeException e) {
Log.e(TAG, "Unable to augment storage stats for " + userHandle, e);
ExceptionUtil.handleException(e);
}
}
}
/**
* Dispatches change notifications if there are any to dispatch.
*
* This method is async; notifications are dispatched onto their own registered executors.
*
*
IMPORTANT: You must always call this within the background task that contains the
* operation that mutated the index. If you called it outside of that task, it could start
* before the task completes, causing notifications to be missed.
*/
@WorkerThread
private void dispatchChangeNotifications(@NonNull AppSearchUserInstance instance) {
instance.getAppSearchImpl().dispatchAndClearChangeNotifications();
}
@WorkerThread
private void checkForOptimize(
@NonNull UserHandle targetUser,
@NonNull AppSearchUserInstance instance,
int mutateBatchSize) {
if (mServiceImplHelper.isUserLocked(targetUser)) {
// We shouldn't schedule any task to locked user.
return;
}
mExecutorManager.getOrCreateUserExecutor(targetUser).execute(() -> {
long totalLatencyStartMillis = SystemClock.elapsedRealtime();
OptimizeStats.Builder builder = new OptimizeStats.Builder();
try {
instance.getAppSearchImpl().checkForOptimize(mutateBatchSize, builder);
} catch (Exception e) {
Log.w(TAG, "Error occurred when check for optimize", e);
} finally {
OptimizeStats oStats = builder
.setTotalLatencyMillis(
(int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis))
.build();
if (oStats.getOriginalDocumentCount() > 0) {
// see if optimize has been run by checking originalDocumentCount
instance.getLogger().logStats(oStats);
}
}
});
}
@WorkerThread
private void checkForOptimize(
@NonNull UserHandle targetUser,
@NonNull AppSearchUserInstance instance) {
if (mServiceImplHelper.isUserLocked(targetUser)) {
// We shouldn't schedule any task to locked user.
return;
}
mExecutorManager.getOrCreateUserExecutor(targetUser).execute(() -> {
long totalLatencyStartMillis = SystemClock.elapsedRealtime();
OptimizeStats.Builder builder = new OptimizeStats.Builder();
try {
instance.getAppSearchImpl().checkForOptimize(builder);
} catch (Exception e) {
Log.w(TAG, "Error occurred when check for optimize", e);
} finally {
OptimizeStats oStats = builder
.setTotalLatencyMillis(
(int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis))
.build();
if (oStats.getOriginalDocumentCount() > 0) {
// see if optimize has been run by checking originalDocumentCount
instance.getLogger().logStats(oStats);
}
}
});
}
/**
* An API call is considered global if the calling package and target package names do not
* match.
*
* Enterprise session calls do not necessarily have access to same-package data; therefore, even
* if the calling and target packages are the same, enterprise session calls must always be
* global to go through the proper visibility checks. (Enterprise session calls are also always
* considered global for CallStats logging.)
*/
private boolean isGlobalCall(@NonNull String callingPackageName,
@NonNull String targetPackageName, boolean isForEnterprise) {
return !callingPackageName.equals(targetPackageName) || isForEnterprise;
}
/**
* Logs rate-limited or denied calls to CallStats.
*/
private void logRateLimitedOrCallDeniedCallStats(@NonNull String callingPackageName,
@Nullable String callingDatabaseName, @CallStats.CallType int apiType,
@NonNull UserHandle targetUser, long binderCallStartTimeMillis,
long totalLatencyStartTimeMillis, int numOperations,
@AppSearchResult.ResultCode int statusCode) {
Objects.requireNonNull(callingPackageName);
Objects.requireNonNull(targetUser);
int estimatedBinderLatencyMillis =
2 * (int) (totalLatencyStartTimeMillis - binderCallStartTimeMillis);
int totalLatencyMillis =
(int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
mAppSearchUserInstanceManager.getUserInstance(targetUser).getLogger().logStats(
new CallStats.Builder()
.setPackageName(callingPackageName)
.setDatabase(callingDatabaseName)
.setStatusCode(statusCode)
.setTotalLatencyMillis(totalLatencyMillis)
.setCallType(apiType)
.setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
.setNumOperationsFailed(numOperations)
.build());
}
/**
* Checks if an API call for a given calling package and calling database should be denied
* according to the denylist. If the call is denied, also logs the denial through CallStats.
*
* @return true if the given api call should be denied for the given calling package and calling
* database; otherwise false
*/
private boolean checkCallDenied(@NonNull String callingPackageName,
@Nullable String callingDatabaseName, @CallStats.CallType int apiType,
@NonNull UserHandle targetUser, long binderCallStartTimeMillis,
long totalLatencyStartTimeMillis, int numOperations) {
Denylist denylist = mAppSearchConfig.getCachedDenylist();
boolean denied = callingDatabaseName == null ? denylist.checkDeniedPackage(
callingPackageName, apiType) : denylist.checkDeniedPackageDatabase(
callingPackageName, callingDatabaseName, apiType);
if (denied) {
logRateLimitedOrCallDeniedCallStats(callingPackageName, callingDatabaseName, apiType,
targetUser, binderCallStartTimeMillis, totalLatencyStartTimeMillis,
numOperations, RESULT_DENIED);
}
return denied;
}
/**
* Checks if an API call for a given calling package and calling database should be denied
* according to the denylist. If the call is denied, also logs the denial through CallStats and
* invokes the given {@link IAppSearchResultCallback} with a failed result.
*
* @return true if the given api call should be denied for the given calling package and calling
* database; otherwise false
*/
private boolean checkCallDenied(@NonNull String callingPackageName,
@Nullable String callingDatabaseName, @CallStats.CallType int apiType,
@NonNull IAppSearchResultCallback callback, @NonNull UserHandle targetUser,
long binderCallStartTimeMillis, long totalLatencyStartTimeMillis, int numOperations) {
if (checkCallDenied(callingPackageName, callingDatabaseName, apiType, targetUser,
binderCallStartTimeMillis, totalLatencyStartTimeMillis, numOperations)) {
invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
AppSearchResult.newFailedResult(RESULT_DENIED, null)));
return true;
}
return false;
}
/**
* Checks if an API call for a given calling package and calling database should be denied
* according to the denylist. If the call is denied, also logs the denial through CallStats and
* invokes the given {@link IAppSearchBatchResultCallback} with a failed result.
*
* @return true if the given api call should be denied for the given calling package and calling
* database; otherwise false
*/
private boolean checkCallDenied(@NonNull String callingPackageName,
@Nullable String callingDatabaseName, @CallStats.CallType int apiType,
@NonNull IAppSearchBatchResultCallback callback, @NonNull UserHandle targetUser,
long binderCallStartTimeMillis, long totalLatencyStartTimeMillis, int numOperations) {
if (checkCallDenied(callingPackageName, callingDatabaseName, apiType, targetUser,
binderCallStartTimeMillis, totalLatencyStartTimeMillis, numOperations)) {
invokeCallbackOnError(callback, AppSearchResult.newFailedResult(RESULT_DENIED, null));
return true;
}
return false;
}
}