Upgrade to API 34 rev 2 of sources
It fixes various issues (e.g. b/296211808) with sources
Test: None
Change-Id: I22c6848dd83f2db9ad5335da57a512d5e5bb6ba3
diff --git a/android-34/META-INF/MANIFEST.MF b/android-34/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..8032f19
--- /dev/null
+++ b/android-34/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Created-By: soong_zip
+
diff --git a/android-34/android/accounts/AccountManagerPerfTest.java b/android-34/android/accounts/AccountManagerPerfTest.java
deleted file mode 100644
index e455e6a..0000000
--- a/android-34/android/accounts/AccountManagerPerfTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.accounts;
-
-import static junit.framework.Assert.fail;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.perftests.utils.BenchmarkState;
-import android.perftests.utils.PerfStatusReporter;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public class AccountManagerPerfTest {
-
- @Rule
- public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
-
- @Test
- public void testGetAccounts() {
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- final Context context = InstrumentationRegistry.getTargetContext();
- if (context.checkSelfPermission(Manifest.permission.GET_ACCOUNTS)
- != PackageManager.PERMISSION_GRANTED) {
- fail("Missing required GET_ACCOUNTS permission");
- }
- AccountManager accountManager = AccountManager.get(context);
- while (state.keepRunning()) {
- accountManager.getAccounts();
- }
- }
-}
diff --git a/android-34/android/adservices/AdServicesFrameworkInitializer.java b/android-34/android/adservices/AdServicesFrameworkInitializer.java
new file mode 100644
index 0000000..abfc7a7
--- /dev/null
+++ b/android-34/android/adservices/AdServicesFrameworkInitializer.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices;
+
+import static android.adservices.adid.AdIdManager.ADID_SERVICE;
+import static android.adservices.adselection.AdSelectionManager.AD_SELECTION_SERVICE;
+import static android.adservices.appsetid.AppSetIdManager.APPSETID_SERVICE;
+import static android.adservices.common.AdServicesCommonManager.AD_SERVICES_COMMON_SERVICE;
+import static android.adservices.customaudience.CustomAudienceManager.CUSTOM_AUDIENCE_SERVICE;
+import static android.adservices.measurement.MeasurementManager.MEASUREMENT_SERVICE;
+import static android.adservices.topics.TopicsManager.TOPICS_SERVICE;
+
+import android.adservices.adid.AdIdManager;
+import android.adservices.adselection.AdSelectionManager;
+import android.adservices.appsetid.AppSetIdManager;
+import android.adservices.common.AdServicesCommonManager;
+import android.adservices.customaudience.CustomAudienceManager;
+import android.adservices.measurement.MeasurementManager;
+import android.adservices.topics.TopicsManager;
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.app.sdksandbox.SdkSandboxSystemServiceRegistry;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.LogUtil;
+
+/**
+ * Class holding initialization code for the AdServices module.
+ *
+ * @hide
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class AdServicesFrameworkInitializer {
+ private AdServicesFrameworkInitializer() {
+ }
+
+ /**
+ * Called by {@link SystemServiceRegistry}'s static initializer and registers all
+ * AdServices services to {@link Context}, so that
+ * {@link Context#getSystemService} can return them.
+ *
+ * @throws IllegalStateException if this is called from anywhere besides
+ * {@link SystemServiceRegistry}
+ */
+ public static void registerServiceWrappers() {
+ LogUtil.d("Registering AdServices's TopicsManager.");
+ SystemServiceRegistry.registerContextAwareService(
+ TOPICS_SERVICE, TopicsManager.class,
+ (c) -> new TopicsManager(c));
+ // TODO(b/242889021): don't use this workaround on devices that have proper fix
+ SdkSandboxSystemServiceRegistry.getInstance()
+ .registerServiceMutator(
+ TOPICS_SERVICE,
+ (service, ctx) -> ((TopicsManager) service).initialize(ctx));
+
+ LogUtil.d("Registering AdServices's CustomAudienceManager.");
+ SystemServiceRegistry.registerContextAwareService(
+ CUSTOM_AUDIENCE_SERVICE, CustomAudienceManager.class, CustomAudienceManager::new);
+ // TODO(b/242889021): don't use this workaround on devices that have proper fix
+ SdkSandboxSystemServiceRegistry.getInstance()
+ .registerServiceMutator(
+ CUSTOM_AUDIENCE_SERVICE,
+ (service, ctx) -> ((CustomAudienceManager) service).initialize(ctx));
+
+ LogUtil.d("Registering AdServices's AdSelectionManager.");
+ SystemServiceRegistry.registerContextAwareService(
+ AD_SELECTION_SERVICE, AdSelectionManager.class, AdSelectionManager::new);
+ // TODO(b/242889021): don't use this workaround on devices that have proper fix
+ SdkSandboxSystemServiceRegistry.getInstance()
+ .registerServiceMutator(
+ AD_SELECTION_SERVICE,
+ (service, ctx) -> ((AdSelectionManager) service).initialize(ctx));
+
+ LogUtil.d("Registering AdServices's MeasurementManager.");
+ SystemServiceRegistry.registerContextAwareService(
+ MEASUREMENT_SERVICE, MeasurementManager.class, MeasurementManager::new);
+ // TODO(b/242889021): don't use this workaround on devices that have proper fix
+ SdkSandboxSystemServiceRegistry.getInstance()
+ .registerServiceMutator(
+ MEASUREMENT_SERVICE,
+ (service, ctx) -> ((MeasurementManager) service).initialize(ctx));
+
+ LogUtil.d("Registering AdServices's AdIdManager.");
+ SystemServiceRegistry.registerContextAwareService(
+ ADID_SERVICE, AdIdManager.class, (c) -> new AdIdManager(c));
+ // TODO(b/242889021): don't use this workaround on devices that have proper fix
+ SdkSandboxSystemServiceRegistry.getInstance()
+ .registerServiceMutator(
+ ADID_SERVICE, (service, ctx) -> ((AdIdManager) service).initialize(ctx));
+
+ LogUtil.d("Registering AdServices's AppSetIdManager.");
+ SystemServiceRegistry.registerContextAwareService(
+ APPSETID_SERVICE, AppSetIdManager.class, (c) -> new AppSetIdManager(c));
+ // TODO(b/242889021): don't use this workaround on devices that have proper fix
+ SdkSandboxSystemServiceRegistry.getInstance()
+ .registerServiceMutator(
+ APPSETID_SERVICE,
+ (service, ctx) -> ((AppSetIdManager) service).initialize(ctx));
+
+ LogUtil.d("Registering AdServices's AdServicesCommonManager.");
+ SystemServiceRegistry.registerContextAwareService(AD_SERVICES_COMMON_SERVICE,
+ AdServicesCommonManager.class,
+ (c) -> new AdServicesCommonManager(c));
+ }
+}
diff --git a/android-34/android/adservices/AdServicesState.java b/android-34/android/adservices/AdServicesState.java
new file mode 100644
index 0000000..42d2c51
--- /dev/null
+++ b/android-34/android/adservices/AdServicesState.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices;
+
+/** This class specifies the state of the APIs exposed by AdServicesApi apk. */
+public class AdServicesState {
+
+ private AdServicesState() {}
+
+ /**
+ * Returns current state of the {@code AdServicesApi}. The state of AdServicesApi may change
+ * only upon reboot, so this value can be cached, but not persisted, i.e., the value should be
+ * rechecked after a reboot.
+ */
+ public static boolean isAdServicesStateEnabled() {
+ return true;
+ }
+}
+
diff --git a/android-34/android/adservices/AdServicesVersion.java b/android-34/android/adservices/AdServicesVersion.java
new file mode 100644
index 0000000..b059b6e
--- /dev/null
+++ b/android-34/android/adservices/AdServicesVersion.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices;
+
+import android.annotation.SuppressLint;
+
+/**
+ * This class specifies the current version of the AdServices API.
+ *
+ * @removed
+ */
+public class AdServicesVersion {
+
+ /**
+ * @hide
+ */
+ public AdServicesVersion() {}
+
+ /**
+ * The API version of this AdServices API.
+ */
+ @SuppressLint("CompileTimeConstant")
+ public static final int API_VERSION;
+
+ // This variable needs to be initialized in static {} , otherwise javac
+ // would inline these constants and they won't be updatable.
+ static {
+ API_VERSION = 2;
+ }
+}
+
diff --git a/android-34/android/adservices/adid/AdId.java b/android-34/android/adservices/adid/AdId.java
new file mode 100644
index 0000000..3190c9d
--- /dev/null
+++ b/android-34/android/adservices/adid/AdId.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.adid;
+
+import android.annotation.NonNull;
+
+import java.util.Objects;
+
+/**
+ * A unique, user-resettable, device-wide, per-profile ID for advertising.
+ *
+ * <p>Ad networks may use {@code AdId} to monetize for Interest Based Advertising (IBA), i.e.
+ * targeting and remarketing ads. The user may limit availability of this identifier.
+ *
+ * @see AdIdManager#getAdId(Executor, OutcomeReceiver)
+ */
+public class AdId {
+ @NonNull private final String mAdId;
+ private final boolean mLimitAdTrackingEnabled;
+
+ /**
+ * A zeroed-out {@link #getAdId ad id} that is returned when the user has {@link
+ * #isLimitAdTrackingEnabled limited ad tracking}.
+ */
+ public static final String ZERO_OUT = "00000000-0000-0000-0000-000000000000";
+
+ /**
+ * Creates an instance of {@link AdId}
+ *
+ * @param adId obtained from the provider service.
+ * @param limitAdTrackingEnabled value from the provider service which determines the value of
+ * adId.
+ */
+ public AdId(@NonNull String adId, boolean limitAdTrackingEnabled) {
+ mAdId = adId;
+ mLimitAdTrackingEnabled = limitAdTrackingEnabled;
+ }
+
+ /**
+ * The advertising ID.
+ *
+ * <p>The value of advertising Id depends on a combination of {@link
+ * #isLimitAdTrackingEnabled()} and {@link
+ * android.adservices.common.AdServicesPermissions#ACCESS_ADSERVICES_AD_ID}.
+ *
+ * <p>When the user is {@link #isLimitAdTrackingEnabled limiting ad tracking}, the API returns
+ * {@link #ZERO_OUT}. This disallows a caller to track the user for monetization purposes.
+ *
+ * <p>Otherwise, a string unique to the device and user is returned, which can be used to track
+ * users for advertising.
+ */
+ public @NonNull String getAdId() {
+ return mAdId;
+ }
+
+ /**
+ * Retrieves the limit ad tracking enabled setting.
+ *
+ * <p>This value is true if user has limit ad tracking enabled, {@code false} otherwise.
+ */
+ public boolean isLimitAdTrackingEnabled() {
+ return mLimitAdTrackingEnabled;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof AdId)) {
+ return false;
+ }
+ AdId that = (AdId) o;
+ return mAdId.equals(that.mAdId)
+ && (mLimitAdTrackingEnabled == that.mLimitAdTrackingEnabled);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAdId, mLimitAdTrackingEnabled);
+ }
+}
diff --git a/android-34/android/adservices/adid/AdIdManager.java b/android-34/android/adservices/adid/AdIdManager.java
new file mode 100644
index 0000000..d267f6e
--- /dev/null
+++ b/android-34/android/adservices/adid/AdIdManager.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.adid;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID;
+
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.CallerMetadata;
+import android.adservices.common.SandboxedSdkContextUtils;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.app.sdksandbox.SandboxedSdkContext;
+import android.content.Context;
+import android.os.Build;
+import android.os.LimitExceededException;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.SystemClock;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.LogUtil;
+import com.android.adservices.ServiceBinder;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * AdId Manager provides APIs for app and ad-SDKs to access advertising ID. The advertising ID is a
+ * unique, per-device, user-resettable ID for advertising. It gives users better controls and
+ * provides developers with a simple, standard system to continue to monetize their apps via
+ * personalized ads (formerly known as interest-based ads).
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public class AdIdManager {
+ /**
+ * Service used for registering AdIdManager in the system service registry.
+ *
+ * @hide
+ */
+ public static final String ADID_SERVICE = "adid_service";
+
+ // When an app calls the AdId API directly, it sets the SDK name to empty string.
+ static final String EMPTY_SDK = "";
+
+ private Context mContext;
+ private ServiceBinder<IAdIdService> mServiceBinder;
+
+ /**
+ * Factory method for creating an instance of AdIdManager.
+ *
+ * @param context The {@link Context} to use
+ * @return A {@link AdIdManager} instance
+ */
+ @NonNull
+ public static AdIdManager get(@NonNull Context context) {
+ // On T+, context.getSystemService() does more than just call constructor.
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ ? context.getSystemService(AdIdManager.class)
+ : new AdIdManager(context);
+ }
+
+ /**
+ * Create AdIdManager
+ *
+ * @hide
+ */
+ public AdIdManager(Context context) {
+ // In case the AdIdManager is initiated from inside a sdk_sandbox process the fields
+ // will be immediately rewritten by the initialize method below.
+ initialize(context);
+ }
+
+ /**
+ * Initializes {@link AdIdManager} with the given {@code context}.
+ *
+ * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context.
+ * For more information check the javadoc on the {@link
+ * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}.
+ *
+ * @hide
+ * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry
+ */
+ public AdIdManager initialize(Context context) {
+ mContext = context;
+ mServiceBinder =
+ ServiceBinder.getServiceBinder(
+ context,
+ AdServicesCommon.ACTION_ADID_SERVICE,
+ IAdIdService.Stub::asInterface);
+ return this;
+ }
+
+ @NonNull
+ private IAdIdService getService() {
+ IAdIdService service = mServiceBinder.getService();
+ if (service == null) {
+ throw new IllegalStateException("Unable to find the service");
+ }
+ return service;
+ }
+
+ @NonNull
+ private Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Return the AdId.
+ *
+ * @param executor The executor to run callback.
+ * @param callback The callback that's called after adid are available or an error occurs.
+ * @throws SecurityException if caller is not authorized to call this API.
+ * @throws IllegalStateException if this API is not available.
+ * @throws LimitExceededException if rate limit was reached.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_AD_ID)
+ @NonNull
+ public void getAdId(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<AdId, Exception> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ CallerMetadata callerMetadata =
+ new CallerMetadata.Builder()
+ .setBinderElapsedTimestamp(SystemClock.elapsedRealtime())
+ .build();
+ final IAdIdService service = getService();
+ String appPackageName = "";
+ String sdkPackageName = "";
+ // First check if context is SandboxedSdkContext or not
+ Context getAdIdRequestContext = getContext();
+ SandboxedSdkContext requestContext =
+ SandboxedSdkContextUtils.getAsSandboxedSdkContext(getAdIdRequestContext);
+ if (requestContext != null) {
+ sdkPackageName = requestContext.getSdkPackageName();
+ appPackageName = requestContext.getClientPackageName();
+ } else { // This is the case without the Sandbox.
+ appPackageName = getAdIdRequestContext.getPackageName();
+ }
+
+ try {
+ service.getAdId(
+ new GetAdIdParam.Builder()
+ .setAppPackageName(appPackageName)
+ .setSdkPackageName(sdkPackageName)
+ .build(),
+ callerMetadata,
+ new IGetAdIdCallback.Stub() {
+ @Override
+ public void onResult(GetAdIdResult resultParcel) {
+ executor.execute(
+ () -> {
+ if (resultParcel.isSuccess()) {
+ callback.onResult(
+ new AdId(
+ resultParcel.getAdId(),
+ resultParcel.isLatEnabled()));
+ } else {
+ callback.onError(
+ AdServicesStatusUtils.asException(
+ resultParcel));
+ }
+ });
+ }
+
+ @Override
+ public void onError(int resultCode) {
+ executor.execute(
+ () ->
+ callback.onError(
+ AdServicesStatusUtils.asException(resultCode)));
+ }
+ });
+ } catch (RemoteException e) {
+ LogUtil.e(e, "RemoteException");
+ callback.onError(e);
+ }
+ }
+
+ /**
+ * If the service is in an APK (as opposed to the system service), unbind it from the service to
+ * allow the APK process to die.
+ *
+ * @hide
+ */
+ // TODO: change to @VisibleForTesting
+ public void unbindFromService() {
+ mServiceBinder.unbindFromService();
+ }
+}
diff --git a/android-34/android/adservices/adid/AdIdProviderService.java b/android-34/android/adservices/adid/AdIdProviderService.java
new file mode 100644
index 0000000..e9781a8
--- /dev/null
+++ b/android-34/android/adservices/adid/AdIdProviderService.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adid;
+
+import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.IOException;
+
+/**
+ * Abstract Base class for provider service to implement generation of Advertising Id and
+ * limitAdTracking value.
+ *
+ * <p>The implementor of this service needs to override the onGetAdId method and provide a
+ * device-level unique advertising Id and limitAdTracking value on that device.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class AdIdProviderService extends Service {
+
+ /** The intent that the service must respond to. Add it to the intent filter of the service. */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.adservices.adid.AdIdProviderService";
+
+ /**
+ * Abstract method which will be overridden by provider to provide the adId. For multi-user,
+ * multi-profiles on-device scenarios, separate instance of service per user is expected to
+ * implement this method.
+ */
+ @NonNull
+ public abstract AdId onGetAdId(int clientUid, @NonNull String clientPackageName)
+ throws IOException;
+
+ private final android.adservices.adid.IAdIdProviderService mInterface =
+ new android.adservices.adid.IAdIdProviderService.Stub() {
+ @Override
+ public void getAdIdProvider(
+ int appUID,
+ @NonNull String packageName,
+ @NonNull IGetAdIdProviderCallback resultCallback)
+ throws RemoteException {
+ try {
+ AdId adId = onGetAdId(appUID, packageName);
+ GetAdIdResult adIdInternal =
+ new GetAdIdResult.Builder()
+ .setStatusCode(STATUS_SUCCESS)
+ .setErrorMessage("")
+ .setAdId(adId.getAdId())
+ .setLatEnabled(adId.isLimitAdTrackingEnabled())
+ .build();
+
+ resultCallback.onResult(adIdInternal);
+ } catch (Throwable e) {
+ resultCallback.onError(e.getMessage());
+ }
+ }
+ };
+
+ @Nullable
+ @Override
+ public final IBinder onBind(@Nullable Intent intent) {
+ return mInterface.asBinder();
+ }
+}
diff --git a/android-34/android/adservices/adid/GetAdIdParam.java b/android-34/android/adservices/adid/GetAdIdParam.java
new file mode 100644
index 0000000..50bf5de
--- /dev/null
+++ b/android-34/android/adservices/adid/GetAdIdParam.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adid;
+
+import static android.adservices.adid.AdIdManager.EMPTY_SDK;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represent input params to the getAdId API.
+ *
+ * @hide
+ */
+public final class GetAdIdParam implements Parcelable {
+ private final String mSdkPackageName;
+ private final String mAppPackageName;
+
+ private GetAdIdParam(@Nullable String sdkPackageName, @NonNull String appPackageName) {
+ mSdkPackageName = sdkPackageName;
+ mAppPackageName = appPackageName;
+ }
+
+ private GetAdIdParam(@NonNull Parcel in) {
+ mSdkPackageName = in.readString();
+ mAppPackageName = in.readString();
+ }
+
+ public static final @NonNull Creator<GetAdIdParam> CREATOR =
+ new Parcelable.Creator<GetAdIdParam>() {
+ @Override
+ public GetAdIdParam createFromParcel(Parcel in) {
+ return new GetAdIdParam(in);
+ }
+
+ @Override
+ public GetAdIdParam[] newArray(int size) {
+ return new GetAdIdParam[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeString(mSdkPackageName);
+ out.writeString(mAppPackageName);
+ }
+
+ /** Get the Sdk Package Name. This is the package name in the Manifest. */
+ @NonNull
+ public String getSdkPackageName() {
+ return mSdkPackageName;
+ }
+
+ /** Get the App PackageName. */
+ @NonNull
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /** Builder for {@link GetAdIdParam} objects. */
+ public static final class Builder {
+ private String mSdkPackageName;
+ private String mAppPackageName;
+
+ public Builder() {}
+
+ /**
+ * Set the Sdk Package Name. When the app calls the AdId API directly without using an SDK,
+ * don't set this field.
+ */
+ public @NonNull Builder setSdkPackageName(@NonNull String sdkPackageName) {
+ mSdkPackageName = sdkPackageName;
+ return this;
+ }
+
+ /** Set the App PackageName. */
+ public @NonNull Builder setAppPackageName(@NonNull String appPackageName) {
+ mAppPackageName = appPackageName;
+ return this;
+ }
+
+ /** Builds a {@link GetAdIdParam} instance. */
+ public @NonNull GetAdIdParam build() {
+ if (mSdkPackageName == null) {
+ // When Sdk package name is not set, we assume the App calls the AdId API
+ // directly.
+ // We set the Sdk package name to empty to mark this.
+ mSdkPackageName = EMPTY_SDK;
+ }
+
+ if (mAppPackageName == null || mAppPackageName.isEmpty()) {
+ throw new IllegalArgumentException("App PackageName must not be empty or null");
+ }
+
+ return new GetAdIdParam(mSdkPackageName, mAppPackageName);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adid/GetAdIdResult.java b/android-34/android/adservices/adid/GetAdIdResult.java
new file mode 100644
index 0000000..bfbd191
--- /dev/null
+++ b/android-34/android/adservices/adid/GetAdIdResult.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adid;
+
+import android.adservices.common.AdServicesResponse;
+import android.adservices.common.AdServicesStatusUtils;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represent the result from the getAdId API.
+ *
+ * @hide
+ */
+public final class GetAdIdResult extends AdServicesResponse {
+ @NonNull private final String mAdId;
+ private final boolean mLimitAdTrackingEnabled;
+
+ private GetAdIdResult(
+ @AdServicesStatusUtils.StatusCode int resultCode,
+ @Nullable String errorMessage,
+ @NonNull String adId,
+ boolean isLimitAdTrackingEnabled) {
+ super(resultCode, errorMessage);
+ mAdId = adId;
+ mLimitAdTrackingEnabled = isLimitAdTrackingEnabled;
+ }
+
+ private GetAdIdResult(@NonNull Parcel in) {
+ super(in);
+ Objects.requireNonNull(in);
+
+ mAdId = in.readString();
+ mLimitAdTrackingEnabled = in.readBoolean();
+ }
+
+ public static final @NonNull Creator<GetAdIdResult> CREATOR =
+ new Parcelable.Creator<GetAdIdResult>() {
+ @Override
+ public GetAdIdResult createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new GetAdIdResult(in);
+ }
+
+ @Override
+ public GetAdIdResult[] newArray(int size) {
+ return new GetAdIdResult[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mStatusCode);
+ out.writeString(mErrorMessage);
+ out.writeString(mAdId);
+ out.writeBoolean(mLimitAdTrackingEnabled);
+ }
+
+ /**
+ * Returns the error message associated with this result.
+ *
+ * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. The error
+ * message may be {@code null} even if {@link #isSuccess} is {@code false}.
+ */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /** Returns the advertising ID associated with this result. */
+ @NonNull
+ public String getAdId() {
+ return mAdId;
+ }
+
+ /** Returns the Limited adtracking field associated with this result. */
+ public boolean isLatEnabled() {
+ return mLimitAdTrackingEnabled;
+ }
+
+ @Override
+ public String toString() {
+ return "GetAdIdResult{"
+ + "mResultCode="
+ + mStatusCode
+ + ", mErrorMessage='"
+ + mErrorMessage
+ + '\''
+ + ", mAdId="
+ + mAdId
+ + ", mLimitAdTrackingEnabled="
+ + mLimitAdTrackingEnabled
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof GetAdIdResult)) {
+ return false;
+ }
+
+ GetAdIdResult that = (GetAdIdResult) o;
+
+ return mStatusCode == that.mStatusCode
+ && Objects.equals(mErrorMessage, that.mErrorMessage)
+ && Objects.equals(mAdId, that.mAdId)
+ && (mLimitAdTrackingEnabled == that.mLimitAdTrackingEnabled);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mStatusCode, mErrorMessage, mAdId, mLimitAdTrackingEnabled);
+ }
+
+ /**
+ * Builder for {@link GetAdIdResult} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ private @AdServicesStatusUtils.StatusCode int mStatusCode;
+ @Nullable private String mErrorMessage;
+ @NonNull private String mAdId;
+ private boolean mLimitAdTrackingEnabled;
+
+ public Builder() {}
+
+ /** Set the Result Code. */
+ public @NonNull Builder setStatusCode(@AdServicesStatusUtils.StatusCode int statusCode) {
+ mStatusCode = statusCode;
+ return this;
+ }
+
+ /** Set the Error Message. */
+ public @NonNull Builder setErrorMessage(@Nullable String errorMessage) {
+ mErrorMessage = errorMessage;
+ return this;
+ }
+
+ /** Set the adid. */
+ public @NonNull Builder setAdId(@NonNull String adId) {
+ mAdId = adId;
+ return this;
+ }
+
+ /** Set the Limited AdTracking enabled field. */
+ public @NonNull Builder setLatEnabled(boolean isLimitAdTrackingEnabled) {
+ mLimitAdTrackingEnabled = isLimitAdTrackingEnabled;
+ return this;
+ }
+
+ /** Builds a {@link GetAdIdResult} instance. */
+ public @NonNull GetAdIdResult build() {
+ if (mAdId == null) {
+ throw new IllegalArgumentException("adId is null");
+ }
+
+ return new GetAdIdResult(mStatusCode, mErrorMessage, mAdId, mLimitAdTrackingEnabled);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/AdSelectionConfig.java b/android-34/android/adservices/adselection/AdSelectionConfig.java
new file mode 100644
index 0000000..5e64257
--- /dev/null
+++ b/android-34/android/adservices/adselection/AdSelectionConfig.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.adselection;
+
+import android.adservices.common.AdSelectionSignals;
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Contains the configuration of the ad selection process.
+ *
+ * <p>Instances of this class are created by SDKs to be provided as arguments to the {@link
+ * AdSelectionManager#selectAds} and {@link AdSelectionManager#reportImpression} methods in {@link
+ * AdSelectionManager}.
+ */
+// TODO(b/233280314): investigate on adSelectionConfig optimization by merging mCustomAudienceBuyers
+// and mPerBuyerSignals.
+public final class AdSelectionConfig implements Parcelable {
+ @NonNull private final AdTechIdentifier mSeller;
+ @NonNull private final Uri mDecisionLogicUri;
+ @NonNull private final List<AdTechIdentifier> mCustomAudienceBuyers;
+ @NonNull private final AdSelectionSignals mAdSelectionSignals;
+ @NonNull private final AdSelectionSignals mSellerSignals;
+ @NonNull private final Map<AdTechIdentifier, AdSelectionSignals> mPerBuyerSignals;
+ @NonNull private final Map<AdTechIdentifier, ContextualAds> mBuyerContextualAds;
+ @NonNull private final Uri mTrustedScoringSignalsUri;
+
+ @NonNull
+ public static final Creator<AdSelectionConfig> CREATOR =
+ new Creator<AdSelectionConfig>() {
+ @Override
+ public AdSelectionConfig createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new AdSelectionConfig(in);
+ }
+
+ @Override
+ public AdSelectionConfig[] newArray(int size) {
+ return new AdSelectionConfig[size];
+ }
+ };
+
+ private AdSelectionConfig(
+ @NonNull AdTechIdentifier seller,
+ @NonNull Uri decisionLogicUri,
+ @NonNull List<AdTechIdentifier> customAudienceBuyers,
+ @NonNull AdSelectionSignals adSelectionSignals,
+ @NonNull AdSelectionSignals sellerSignals,
+ @NonNull Map<AdTechIdentifier, AdSelectionSignals> perBuyerSignals,
+ @NonNull Map<AdTechIdentifier, ContextualAds> perBuyerContextualAds,
+ @NonNull Uri trustedScoringSignalsUri) {
+ this.mSeller = seller;
+ this.mDecisionLogicUri = decisionLogicUri;
+ this.mCustomAudienceBuyers = customAudienceBuyers;
+ this.mAdSelectionSignals = adSelectionSignals;
+ this.mSellerSignals = sellerSignals;
+ this.mPerBuyerSignals = perBuyerSignals;
+ this.mBuyerContextualAds = perBuyerContextualAds;
+ this.mTrustedScoringSignalsUri = trustedScoringSignalsUri;
+ }
+
+ private AdSelectionConfig(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ mSeller = AdTechIdentifier.CREATOR.createFromParcel(in);
+ mDecisionLogicUri = Uri.CREATOR.createFromParcel(in);
+ mCustomAudienceBuyers = in.createTypedArrayList(AdTechIdentifier.CREATOR);
+ mAdSelectionSignals = AdSelectionSignals.CREATOR.createFromParcel(in);
+ mSellerSignals = AdSelectionSignals.CREATOR.createFromParcel(in);
+ mPerBuyerSignals =
+ AdServicesParcelableUtil.readMapFromParcel(
+ in, AdTechIdentifier::fromString, AdSelectionSignals.class);
+ mBuyerContextualAds =
+ AdServicesParcelableUtil.readMapFromParcel(
+ in, AdTechIdentifier::fromString, ContextualAds.class);
+ mTrustedScoringSignalsUri = Uri.CREATOR.createFromParcel(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ mSeller.writeToParcel(dest, flags);
+ mDecisionLogicUri.writeToParcel(dest, flags);
+ dest.writeTypedList(mCustomAudienceBuyers);
+ mAdSelectionSignals.writeToParcel(dest, flags);
+ mSellerSignals.writeToParcel(dest, flags);
+ AdServicesParcelableUtil.writeMapToParcel(dest, mPerBuyerSignals);
+ AdServicesParcelableUtil.writeMapToParcel(dest, mBuyerContextualAds);
+ mTrustedScoringSignalsUri.writeToParcel(dest, flags);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AdSelectionConfig)) return false;
+ AdSelectionConfig that = (AdSelectionConfig) o;
+ return Objects.equals(mSeller, that.mSeller)
+ && Objects.equals(mDecisionLogicUri, that.mDecisionLogicUri)
+ && Objects.equals(mCustomAudienceBuyers, that.mCustomAudienceBuyers)
+ && Objects.equals(mAdSelectionSignals, that.mAdSelectionSignals)
+ && Objects.equals(mSellerSignals, that.mSellerSignals)
+ && Objects.equals(mPerBuyerSignals, that.mPerBuyerSignals)
+ && Objects.equals(mBuyerContextualAds, that.mBuyerContextualAds)
+ && Objects.equals(mTrustedScoringSignalsUri, that.mTrustedScoringSignalsUri);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mSeller,
+ mDecisionLogicUri,
+ mCustomAudienceBuyers,
+ mAdSelectionSignals,
+ mSellerSignals,
+ mPerBuyerSignals,
+ mBuyerContextualAds,
+ mTrustedScoringSignalsUri);
+ }
+
+ /**
+ * @return a new builder instance created from this object's cloned data
+ * @hide
+ */
+ @NonNull
+ public AdSelectionConfig.Builder cloneToBuilder() {
+ return new AdSelectionConfig.Builder()
+ .setSeller(this.getSeller())
+ .setBuyerContextualAds(this.getBuyerContextualAds())
+ .setAdSelectionSignals(this.getAdSelectionSignals())
+ .setCustomAudienceBuyers(this.getCustomAudienceBuyers())
+ .setDecisionLogicUri(this.getDecisionLogicUri())
+ .setPerBuyerSignals(this.getPerBuyerSignals())
+ .setSellerSignals(this.getSellerSignals())
+ .setTrustedScoringSignalsUri(this.getTrustedScoringSignalsUri());
+ }
+
+ /** @return a AdTechIdentifier of the seller, for example "www.example-ssp.com" */
+ @NonNull
+ public AdTechIdentifier getSeller() {
+ return mSeller;
+ }
+
+ /**
+ * @return the URI used to retrieve the JS code containing the seller/SSP scoreAd function used
+ * during the ad selection and reporting processes
+ */
+ @NonNull
+ public Uri getDecisionLogicUri() {
+ return mDecisionLogicUri;
+ }
+
+ /**
+ * @return a list of custom audience buyers allowed by the SSP to participate in the ad
+ * selection process
+ */
+ @NonNull
+ public List<AdTechIdentifier> getCustomAudienceBuyers() {
+ return mCustomAudienceBuyers;
+ }
+
+ /**
+ * @return JSON in an AdSelectionSignals object, fetched from the AdSelectionConfig and consumed
+ * by the JS logic fetched from the DSP, represents signals given to the participating
+ * buyers in the ad selection and reporting processes.
+ */
+ @NonNull
+ public AdSelectionSignals getAdSelectionSignals() {
+ return mAdSelectionSignals;
+ }
+
+ /**
+ * @return JSON in an AdSelectionSignals object, provided by the SSP and consumed by the JS
+ * logic fetched from the SSP, represents any information that the SSP used in the ad
+ * scoring process to tweak the results of the ad selection process (e.g. brand safety
+ * checks, excluded contextual ads).
+ */
+ @NonNull
+ public AdSelectionSignals getSellerSignals() {
+ return mSellerSignals;
+ }
+
+ /**
+ * @return a Map of buyers and AdSelectionSignals, fetched from the AdSelectionConfig and
+ * consumed by the JS logic fetched from the DSP, representing any information that each
+ * buyer would provide during ad selection to participants (such as bid floor, ad selection
+ * type, etc.)
+ */
+ @NonNull
+ public Map<AdTechIdentifier, AdSelectionSignals> getPerBuyerSignals() {
+ return mPerBuyerSignals;
+ }
+
+ /**
+ * @return a Map of buyers and corresponding Contextual Ads, these ads are expected to be
+ * pre-downloaded from the contextual path and injected into Ad Selection.
+ * @hide
+ */
+ @NonNull
+ public Map<AdTechIdentifier, ContextualAds> getBuyerContextualAds() {
+ return mBuyerContextualAds;
+ }
+
+ /**
+ * @return URI endpoint of sell-side trusted signal from which creative specific realtime
+ * information can be fetched from.
+ */
+ @NonNull
+ public Uri getTrustedScoringSignalsUri() {
+ return mTrustedScoringSignalsUri;
+ }
+
+ /** Builder for {@link AdSelectionConfig} object. */
+ public static final class Builder {
+ private AdTechIdentifier mSeller;
+ private Uri mDecisionLogicUri;
+ private List<AdTechIdentifier> mCustomAudienceBuyers;
+ private AdSelectionSignals mAdSelectionSignals = AdSelectionSignals.EMPTY;
+ private AdSelectionSignals mSellerSignals = AdSelectionSignals.EMPTY;
+ private Map<AdTechIdentifier, AdSelectionSignals> mPerBuyerSignals = Collections.emptyMap();
+ private Map<AdTechIdentifier, ContextualAds> mBuyerContextualAds = Collections.emptyMap();
+ private Uri mTrustedScoringSignalsUri;
+
+ public Builder() {}
+
+ /**
+ * Sets the seller identifier.
+ *
+ * <p>See {@link #getSeller()} for more details.
+ */
+ @NonNull
+ public AdSelectionConfig.Builder setSeller(@NonNull AdTechIdentifier seller) {
+ Objects.requireNonNull(seller);
+
+ this.mSeller = seller;
+ return this;
+ }
+
+ /**
+ * Sets the URI used to fetch decision logic for use in the ad selection process.
+ *
+ * <p>See {@link #getDecisionLogicUri()} for more details.
+ */
+ @NonNull
+ public AdSelectionConfig.Builder setDecisionLogicUri(@NonNull Uri decisionLogicUri) {
+ Objects.requireNonNull(decisionLogicUri);
+
+ this.mDecisionLogicUri = decisionLogicUri;
+ return this;
+ }
+
+ /**
+ * Sets the list of allowed buyers.
+ *
+ * <p>See {@link #getCustomAudienceBuyers()} for more details.
+ */
+ @NonNull
+ public AdSelectionConfig.Builder setCustomAudienceBuyers(
+ @NonNull List<AdTechIdentifier> customAudienceBuyers) {
+ Objects.requireNonNull(customAudienceBuyers);
+
+ this.mCustomAudienceBuyers = customAudienceBuyers;
+ return this;
+ }
+
+ /**
+ * Sets the signals provided to buyers during ad selection bid generation.
+ *
+ * <p>If not set, defaults to the empty JSON.
+ *
+ * <p>See {@link #getAdSelectionSignals()} for more details.
+ */
+ @NonNull
+ public AdSelectionConfig.Builder setAdSelectionSignals(
+ @NonNull AdSelectionSignals adSelectionSignals) {
+ Objects.requireNonNull(adSelectionSignals);
+
+ this.mAdSelectionSignals = adSelectionSignals;
+ return this;
+ }
+
+ /**
+ * Set the signals used to modify ad selection results.
+ *
+ * <p>If not set, defaults to the empty JSON.
+ *
+ * <p>See {@link #getSellerSignals()} for more details.
+ */
+ @NonNull
+ public AdSelectionConfig.Builder setSellerSignals(
+ @NonNull AdSelectionSignals sellerSignals) {
+ Objects.requireNonNull(sellerSignals);
+
+ this.mSellerSignals = sellerSignals;
+ return this;
+ }
+
+ /**
+ * Sets the signals provided by each buyer during ad selection.
+ *
+ * <p>If not set, defaults to an empty map.
+ *
+ * <p>See {@link #getPerBuyerSignals()} for more details.
+ */
+ @NonNull
+ public AdSelectionConfig.Builder setPerBuyerSignals(
+ @NonNull Map<AdTechIdentifier, AdSelectionSignals> perBuyerSignals) {
+ Objects.requireNonNull(perBuyerSignals);
+
+ this.mPerBuyerSignals = perBuyerSignals;
+ return this;
+ }
+
+ /**
+ * Sets the contextual Ads corresponding to each buyer during ad selection.
+ *
+ * <p>If not set, defaults to an empty map.
+ *
+ * <p>See {@link #getBuyerContextualAds()} ()} for more details.
+ *
+ * @hide
+ */
+ @NonNull
+ public AdSelectionConfig.Builder setBuyerContextualAds(
+ @NonNull Map<AdTechIdentifier, ContextualAds> buyerContextualAds) {
+ Objects.requireNonNull(buyerContextualAds);
+
+ this.mBuyerContextualAds = buyerContextualAds;
+ return this;
+ }
+
+ /**
+ * Sets the URI endpoint of sell-side trusted signal from which creative specific realtime
+ * information can be fetched from.
+ *
+ * <p>If {@link Uri#EMPTY} is passed then network call will be skipped and {@link
+ * AdSelectionSignals#EMPTY} will be passed to ad selection.
+ *
+ * <p>See {@link #getTrustedScoringSignalsUri()} for more details.
+ */
+ @NonNull
+ public AdSelectionConfig.Builder setTrustedScoringSignalsUri(
+ @NonNull Uri trustedScoringSignalsUri) {
+ Objects.requireNonNull(trustedScoringSignalsUri);
+
+ this.mTrustedScoringSignalsUri = trustedScoringSignalsUri;
+ return this;
+ }
+
+ /**
+ * Builds an {@link AdSelectionConfig} instance.
+ *
+ * @throws NullPointerException if any required params are null
+ */
+ @NonNull
+ public AdSelectionConfig build() {
+ Objects.requireNonNull(mSeller);
+ Objects.requireNonNull(mDecisionLogicUri);
+ Objects.requireNonNull(mCustomAudienceBuyers);
+ Objects.requireNonNull(mAdSelectionSignals);
+ Objects.requireNonNull(mSellerSignals);
+ Objects.requireNonNull(mPerBuyerSignals);
+ Objects.requireNonNull(mBuyerContextualAds);
+ Objects.requireNonNull(mTrustedScoringSignalsUri);
+ return new AdSelectionConfig(
+ mSeller,
+ mDecisionLogicUri,
+ mCustomAudienceBuyers,
+ mAdSelectionSignals,
+ mSellerSignals,
+ mPerBuyerSignals,
+ mBuyerContextualAds,
+ mTrustedScoringSignalsUri);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/AdSelectionFromOutcomesConfig.java b/android-34/android/adservices/adselection/AdSelectionFromOutcomesConfig.java
new file mode 100644
index 0000000..8bdc3b2
--- /dev/null
+++ b/android-34/android/adservices/adselection/AdSelectionFromOutcomesConfig.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.adservices.common.AdSelectionSignals;
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Contains the configuration of the ad selection process that select a winner from a given list of
+ * ad selection ids.
+ *
+ * <p>Instances of this class are created by SDKs to be provided as arguments to the {@link
+ * AdSelectionManager#selectAds} methods in {@link AdSelectionManager}.
+ *
+ * @hide
+ */
+public final class AdSelectionFromOutcomesConfig implements Parcelable {
+ @NonNull private final AdTechIdentifier mSeller;
+ @NonNull private final List<Long> mAdSelectionIds;
+ @NonNull private final AdSelectionSignals mSelectionSignals;
+ @NonNull private final Uri mSelectionLogicUri;
+
+ @NonNull
+ public static final Creator<AdSelectionFromOutcomesConfig> CREATOR =
+ new Creator<AdSelectionFromOutcomesConfig>() {
+ @Override
+ public AdSelectionFromOutcomesConfig createFromParcel(@NonNull Parcel in) {
+ return new AdSelectionFromOutcomesConfig(in);
+ }
+
+ @Override
+ public AdSelectionFromOutcomesConfig[] newArray(int size) {
+ return new AdSelectionFromOutcomesConfig[size];
+ }
+ };
+
+ private AdSelectionFromOutcomesConfig(
+ @NonNull AdTechIdentifier seller,
+ @NonNull List<Long> adSelectionIds,
+ @NonNull AdSelectionSignals selectionSignals,
+ @NonNull Uri selectionLogicUri) {
+ Objects.requireNonNull(seller);
+ Objects.requireNonNull(adSelectionIds);
+ Objects.requireNonNull(selectionSignals);
+ Objects.requireNonNull(selectionLogicUri);
+
+ this.mSeller = seller;
+ this.mAdSelectionIds = adSelectionIds;
+ this.mSelectionSignals = selectionSignals;
+ this.mSelectionLogicUri = selectionLogicUri;
+ }
+
+ private AdSelectionFromOutcomesConfig(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ this.mSeller = AdTechIdentifier.CREATOR.createFromParcel(in);
+ this.mAdSelectionIds =
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
+ ? in.readArrayList(Long.class.getClassLoader())
+ : in.readArrayList(Long.class.getClassLoader(), Long.class);
+ this.mSelectionSignals = AdSelectionSignals.CREATOR.createFromParcel(in);
+ this.mSelectionLogicUri = Uri.CREATOR.createFromParcel(in);
+ }
+
+ /** @return a AdTechIdentifier of the seller, for example "www.example-ssp.com" */
+ @NonNull
+ public AdTechIdentifier getSeller() {
+ return mSeller;
+ }
+
+ /**
+ * @return a list of ad selection ids passed by the SSP to participate in the ad selection from
+ * outcomes process
+ */
+ @NonNull
+ public List<Long> getAdSelectionIds() {
+ return mAdSelectionIds;
+ }
+
+ /**
+ * @return JSON in an {@link AdSelectionSignals} object, fetched from the {@link
+ * AdSelectionFromOutcomesConfig} and consumed by the JS logic fetched from the DSP {@code
+ * SelectionLogicUri}.
+ */
+ @NonNull
+ public AdSelectionSignals getSelectionSignals() {
+ return mSelectionSignals;
+ }
+
+ /**
+ * @return the URI used to retrieve the JS code containing the seller/SSP {@code selectOutcome}
+ * function used during the ad selection
+ */
+ @NonNull
+ public Uri getSelectionLogicUri() {
+ return mSelectionLogicUri;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ mSeller.writeToParcel(dest, flags);
+ dest.writeList(mAdSelectionIds);
+ mSelectionSignals.writeToParcel(dest, flags);
+ mSelectionLogicUri.writeToParcel(dest, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AdSelectionFromOutcomesConfig)) return false;
+ AdSelectionFromOutcomesConfig that = (AdSelectionFromOutcomesConfig) o;
+ return Objects.equals(this.mSeller, that.mSeller)
+ && Objects.equals(this.mAdSelectionIds, that.mAdSelectionIds)
+ && Objects.equals(this.mSelectionSignals, that.mSelectionSignals)
+ && Objects.equals(this.mSelectionLogicUri, that.mSelectionLogicUri);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSeller, mAdSelectionIds, mSelectionSignals, mSelectionLogicUri);
+ }
+
+ /**
+ * Builder for {@link AdSelectionFromOutcomesConfig} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @Nullable private AdTechIdentifier mSeller;
+ @Nullable private List<Long> mAdSelectionIds;
+ @Nullable private AdSelectionSignals mSelectionSignals;
+ @Nullable private Uri mSelectionLogicUri;
+
+ public Builder() {}
+
+ /** Sets the seller {@link AdTechIdentifier}. */
+ @NonNull
+ public AdSelectionFromOutcomesConfig.Builder setSeller(@NonNull AdTechIdentifier seller) {
+ Objects.requireNonNull(seller);
+
+ this.mSeller = seller;
+ return this;
+ }
+
+ /** Sets the list of {@code AdSelectionIds} to participate in the selection process. */
+ @NonNull
+ public AdSelectionFromOutcomesConfig.Builder setAdSelectionIds(
+ @NonNull List<Long> adSelectionIds) {
+ Objects.requireNonNull(adSelectionIds);
+
+ this.mAdSelectionIds = adSelectionIds;
+ return this;
+ }
+
+ /**
+ * Sets the {@code SelectionSignals} to be consumed by the JS script downloaded from {@code
+ * SelectionLogicUri}
+ */
+ @NonNull
+ public AdSelectionFromOutcomesConfig.Builder setSelectionSignals(
+ @NonNull AdSelectionSignals selectionSignals) {
+ Objects.requireNonNull(selectionSignals);
+
+ this.mSelectionSignals = selectionSignals;
+ return this;
+ }
+
+ /**
+ * Sets the {@code SelectionLogicUri}. Selection URI could be either of the two schemas:
+ *
+ * <ul>
+ * <li><b>HTTPS:</b> HTTPS URIs have to be absolute URIs where the host matches the {@code
+ * seller}
+ * <li><b>Ad Selection Prebuilt:</b> Ad Selection Service URIs follow {@code
+ * ad-selection-prebuilt://ad-selection-from-outcomes/<name>?<script-generation-parameters>}
+ * format. FLEDGE generates the appropriate JS script without the need for a network
+ * call.
+ * <p>Available prebuilt scripts:
+ * <ul>
+ * <li><b>{@code waterfall-mediation-truncation} for {@code selectOutcome}:</b> This
+ * JS implements Waterfall mediation truncation logic. Mediation SDK's ad is
+ * returned if its bid greater than or equal to the bid floor. Below
+ * parameter(s) are required to use this prebuilt:
+ * <ul>
+ * <li><b>{@code bidFloor}:</b> Key of the bid floor value passed in the
+ * {@link AdSelectionFromOutcomesConfig#getSelectionSignals()} that will
+ * be compared against mediation SDK's winner ad.
+ * </ul>
+ * <p>Ex. If your selection signals look like {@code {"bid_floor": 10}} then,
+ * {@code
+ * ad-selection-prebuilt://ad-selection-from-outcomes/waterfall-mediation-truncation/?bidFloor=bid_floor}
+ * </ul>
+ * </ul>
+ *
+ * {@code AdSelectionIds} and {@code SelectionSignals}.
+ */
+ @NonNull
+ public AdSelectionFromOutcomesConfig.Builder setSelectionLogicUri(
+ @NonNull Uri selectionLogicUri) {
+ Objects.requireNonNull(selectionLogicUri);
+
+ this.mSelectionLogicUri = selectionLogicUri;
+ return this;
+ }
+
+ /** Builds a {@link AdSelectionFromOutcomesConfig} instance. */
+ @NonNull
+ public AdSelectionFromOutcomesConfig build() {
+ Objects.requireNonNull(mSeller);
+ Objects.requireNonNull(mAdSelectionIds);
+ Objects.requireNonNull(mSelectionSignals);
+ Objects.requireNonNull(mSelectionLogicUri);
+
+ return new AdSelectionFromOutcomesConfig(
+ mSeller, mAdSelectionIds, mSelectionSignals, mSelectionLogicUri);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/AdSelectionFromOutcomesInput.java b/android-34/android/adservices/adselection/AdSelectionFromOutcomesInput.java
new file mode 100644
index 0000000..8e72a07
--- /dev/null
+++ b/android-34/android/adservices/adselection/AdSelectionFromOutcomesInput.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represents input parameters to the {@link
+ * com.android.adservices.service.adselection.AdSelectionServiceImpl#selectAdsFromOutcomes} API.
+ *
+ * @hide
+ */
+public final class AdSelectionFromOutcomesInput implements Parcelable {
+ @NonNull private final AdSelectionFromOutcomesConfig mAdSelectionFromOutcomesConfig;
+ @NonNull private final String mCallerPackageName;
+
+ @NonNull
+ public static final Creator<AdSelectionFromOutcomesInput> CREATOR =
+ new Creator<AdSelectionFromOutcomesInput>() {
+ @Override
+ public AdSelectionFromOutcomesInput createFromParcel(@NonNull Parcel source) {
+ return new AdSelectionFromOutcomesInput(source);
+ }
+
+ @Override
+ public AdSelectionFromOutcomesInput[] newArray(int size) {
+ return new AdSelectionFromOutcomesInput[size];
+ }
+ };
+
+ private AdSelectionFromOutcomesInput(
+ @NonNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig,
+ @NonNull String callerPackageName) {
+ Objects.requireNonNull(adSelectionFromOutcomesConfig);
+ Objects.requireNonNull(callerPackageName);
+
+ this.mAdSelectionFromOutcomesConfig = adSelectionFromOutcomesConfig;
+ this.mCallerPackageName = callerPackageName;
+ }
+
+ private AdSelectionFromOutcomesInput(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ this.mAdSelectionFromOutcomesConfig =
+ AdSelectionFromOutcomesConfig.CREATOR.createFromParcel(in);
+ this.mCallerPackageName = in.readString();
+ }
+
+ @NonNull
+ public AdSelectionFromOutcomesConfig getAdSelectionFromOutcomesConfig() {
+ return mAdSelectionFromOutcomesConfig;
+ }
+
+ @NonNull
+ public String getCallerPackageName() {
+ return mCallerPackageName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AdSelectionFromOutcomesInput)) return false;
+ AdSelectionFromOutcomesInput that = (AdSelectionFromOutcomesInput) o;
+ return Objects.equals(
+ this.mAdSelectionFromOutcomesConfig, that.mAdSelectionFromOutcomesConfig)
+ && Objects.equals(this.mCallerPackageName, that.mCallerPackageName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAdSelectionFromOutcomesConfig, mCallerPackageName);
+ }
+
+ /**
+ * Describe the kinds of special objects contained in this Parcelable instance's marshaled
+ * representation. For example, if the object will include a file descriptor in the output of
+ * {@link #writeToParcel(Parcel, int)}, the return value of this method must include the {@link
+ * #CONTENTS_FILE_DESCRIPTOR} bit.
+ *
+ * @return a bitmask indicating the set of special object types marshaled by this Parcelable
+ * object instance.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written. May be 0 or {@link
+ * #PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ mAdSelectionFromOutcomesConfig.writeToParcel(dest, flags);
+ dest.writeString(mCallerPackageName);
+ }
+
+ /**
+ * Builder for {@link AdSelectionFromOutcomesInput} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @Nullable private AdSelectionFromOutcomesConfig mAdSelectionFromOutcomesConfig;
+ @Nullable private String mCallerPackageName;
+
+ public Builder() {}
+
+ /** Sets the {@link AdSelectionFromOutcomesConfig}. */
+ @NonNull
+ public AdSelectionFromOutcomesInput.Builder setAdSelectionFromOutcomesConfig(
+ @NonNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig) {
+ Objects.requireNonNull(adSelectionFromOutcomesConfig);
+
+ this.mAdSelectionFromOutcomesConfig = adSelectionFromOutcomesConfig;
+ return this;
+ }
+
+ /** Sets the caller's package name. */
+ @NonNull
+ public AdSelectionFromOutcomesInput.Builder setCallerPackageName(
+ @NonNull String callerPackageName) {
+ Objects.requireNonNull(callerPackageName);
+
+ this.mCallerPackageName = callerPackageName;
+ return this;
+ }
+
+ /** Builds a {@link AdSelectionFromOutcomesInput} instance. */
+ @NonNull
+ public AdSelectionFromOutcomesInput build() {
+ Objects.requireNonNull(mAdSelectionFromOutcomesConfig);
+ Objects.requireNonNull(mCallerPackageName);
+
+ return new AdSelectionFromOutcomesInput(
+ mAdSelectionFromOutcomesConfig, mCallerPackageName);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/AdSelectionInput.java b/android-34/android/adservices/adselection/AdSelectionInput.java
new file mode 100644
index 0000000..b926f58
--- /dev/null
+++ b/android-34/android/adservices/adselection/AdSelectionInput.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represent input params to the RunAdSelectionInput API.
+ *
+ * @hide
+ */
+public final class AdSelectionInput implements Parcelable {
+ @Nullable private final AdSelectionConfig mAdSelectionConfig;
+ @Nullable private final String mCallerPackageName;
+
+ @NonNull
+ public static final Creator<AdSelectionInput> CREATOR =
+ new Creator<AdSelectionInput>() {
+ public AdSelectionInput createFromParcel(Parcel in) {
+ return new AdSelectionInput(in);
+ }
+
+ public AdSelectionInput[] newArray(int size) {
+ return new AdSelectionInput[size];
+ }
+ };
+
+ private AdSelectionInput(
+ @NonNull AdSelectionConfig adSelectionConfig, @NonNull String callerPackageName) {
+ Objects.requireNonNull(adSelectionConfig);
+
+ this.mAdSelectionConfig = adSelectionConfig;
+ this.mCallerPackageName = callerPackageName;
+ }
+
+ private AdSelectionInput(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ this.mAdSelectionConfig = AdSelectionConfig.CREATOR.createFromParcel(in);
+ this.mCallerPackageName = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ mAdSelectionConfig.writeToParcel(dest, flags);
+ dest.writeString(mCallerPackageName);
+ }
+
+ /**
+ * Returns the adSelectionConfig, one of the inputs to {@link AdSelectionInput} as noted in
+ * {@code AdSelectionService}.
+ */
+ @NonNull
+ public AdSelectionConfig getAdSelectionConfig() {
+ return mAdSelectionConfig;
+ }
+
+ /** @return the caller package name */
+ @NonNull
+ public String getCallerPackageName() {
+ return mCallerPackageName;
+ }
+
+ /**
+ * Builder for {@link AdSelectionInput} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @Nullable private AdSelectionConfig mAdSelectionConfig;
+ @Nullable private String mCallerPackageName;
+
+ public Builder() {}
+
+ /** Set the AdSelectionConfig. */
+ @NonNull
+ public AdSelectionInput.Builder setAdSelectionConfig(
+ @NonNull AdSelectionConfig adSelectionConfig) {
+ Objects.requireNonNull(adSelectionConfig);
+
+ this.mAdSelectionConfig = adSelectionConfig;
+ return this;
+ }
+
+ /** Sets the caller's package name. */
+ @NonNull
+ public AdSelectionInput.Builder setCallerPackageName(@NonNull String callerPackageName) {
+ Objects.requireNonNull(callerPackageName);
+
+ this.mCallerPackageName = callerPackageName;
+ return this;
+ }
+
+ /** Builds a {@link AdSelectionInput} instance. */
+ @NonNull
+ public AdSelectionInput build() {
+ Objects.requireNonNull(mAdSelectionConfig);
+ Objects.requireNonNull(mCallerPackageName);
+
+ return new AdSelectionInput(mAdSelectionConfig, mCallerPackageName);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/AdSelectionManager.java b/android-34/android/adservices/adselection/AdSelectionManager.java
new file mode 100644
index 0000000..00259ab
--- /dev/null
+++ b/android-34/android/adservices/adselection/AdSelectionManager.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE;
+
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.CallerMetadata;
+import android.adservices.common.FledgeErrorResponse;
+import android.adservices.common.SandboxedSdkContextUtils;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.app.sdksandbox.SandboxedSdkContext;
+import android.content.Context;
+import android.os.Build;
+import android.os.LimitExceededException;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.TransactionTooLargeException;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.LoggerFactory;
+import com.android.adservices.ServiceBinder;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * AdSelection Manager provides APIs for app and ad-SDKs to run ad selection processes as well as
+ * report impressions.
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public class AdSelectionManager {
+ private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
+ /**
+ * Constant that represents the service name for {@link AdSelectionManager} to be used in {@link
+ * android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers}
+ *
+ * @hide
+ */
+ public static final String AD_SELECTION_SERVICE = "ad_selection_service";
+
+ @NonNull private Context mContext;
+ @NonNull private ServiceBinder<AdSelectionService> mServiceBinder;
+
+ /**
+ * Factory method for creating an instance of AdSelectionManager.
+ *
+ * @param context The {@link Context} to use
+ * @return A {@link AdSelectionManager} instance
+ */
+ @NonNull
+ public static AdSelectionManager get(@NonNull Context context) {
+ // On T+, context.getSystemService() does more than just call constructor.
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ ? context.getSystemService(AdSelectionManager.class)
+ : new AdSelectionManager(context);
+ }
+
+ /**
+ * Create AdSelectionManager
+ *
+ * @hide
+ */
+ public AdSelectionManager(@NonNull Context context) {
+ Objects.requireNonNull(context);
+
+ // In case the AdSelectionManager is initiated from inside a sdk_sandbox process the
+ // fields will be immediately rewritten by the initialize method below.
+ initialize(context);
+ }
+
+ /**
+ * Initializes {@link AdSelectionManager} with the given {@code context}.
+ *
+ * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context.
+ * For more information check the javadoc on the {@link
+ * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}.
+ *
+ * @hide
+ * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry
+ */
+ public AdSelectionManager initialize(@NonNull Context context) {
+ Objects.requireNonNull(context);
+
+ mContext = context;
+ mServiceBinder =
+ ServiceBinder.getServiceBinder(
+ context,
+ AdServicesCommon.ACTION_AD_SELECTION_SERVICE,
+ AdSelectionService.Stub::asInterface);
+ return this;
+ }
+
+ @NonNull
+ public TestAdSelectionManager getTestAdSelectionManager() {
+ return new TestAdSelectionManager(this);
+ }
+
+ @NonNull
+ AdSelectionService getService() {
+ return mServiceBinder.getService();
+ }
+
+ /**
+ * Runs the ad selection process on device to select a remarketing ad for the caller
+ * application.
+ *
+ * <p>The input {@code adSelectionConfig} is provided by the Ads SDK and the {@link
+ * AdSelectionConfig} object is transferred via a Binder call. For this reason, the total size
+ * of these objects is bound to the Android IPC limitations. Failures to transfer the {@link
+ * AdSelectionConfig} will throws an {@link TransactionTooLargeException}.
+ *
+ * <p>The output is passed by the receiver, which either returns an {@link AdSelectionOutcome}
+ * for a successful run, or an {@link Exception} includes the type of the exception thrown and
+ * the corresponding error message.
+ *
+ * <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
+ * the API received to run the ad selection.
+ *
+ * <p>If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection
+ * services.", it is caused by an internal failure of the ad selection service.
+ *
+ * <p>If the {@link TimeoutException} is thrown, it is caused when a timeout is encountered
+ * during bidding, scoring, or overall selection process to find winning Ad.
+ *
+ * <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
+ * exceeds the allowed rate limits and is throttled.
+ *
+ * <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
+ * or permission is not requested.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void selectAds(
+ @NonNull AdSelectionConfig adSelectionConfig,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<AdSelectionOutcome, Exception> receiver) {
+ Objects.requireNonNull(adSelectionConfig);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = getService();
+ service.selectAds(
+ new AdSelectionInput.Builder()
+ .setAdSelectionConfig(adSelectionConfig)
+ .setCallerPackageName(getCallerPackageName())
+ .build(),
+ new CallerMetadata.Builder()
+ .setBinderElapsedTimestamp(SystemClock.elapsedRealtime())
+ .build(),
+ new AdSelectionCallback.Stub() {
+ @Override
+ public void onSuccess(AdSelectionResponse resultParcel) {
+ executor.execute(
+ () ->
+ receiver.onResult(
+ new AdSelectionOutcome.Builder()
+ .setAdSelectionId(
+ resultParcel.getAdSelectionId())
+ .setRenderUri(
+ resultParcel.getRenderUri())
+ .build()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () -> {
+ receiver.onError(
+ AdServicesStatusUtils.asException(failureParcel));
+ });
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Failure of AdSelection service.");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Selects an ad from the results of previously ran ad selections.
+ *
+ * <p>The input {@code adSelectionFromOutcomesConfig} is provided by the Ads SDK and the {@link
+ * AdSelectionFromOutcomesConfig} object is transferred via a Binder call. For this reason, the
+ * total size of these objects is bound to the Android IPC limitations. Failures to transfer the
+ * {@link AdSelectionFromOutcomesConfig} will throws an {@link TransactionTooLargeException}.
+ *
+ * <p>The output is passed by the receiver, which either returns an {@link AdSelectionOutcome}
+ * for a successful run, or an {@link Exception} includes the type of the exception thrown and
+ * the corresponding error message.
+ *
+ * <p>The input {@code adSelectionFromOutcomesConfig} contains:
+ *
+ * <ul>
+ * <li>{@code Seller} is required to be a registered {@link
+ * android.adservices.common.AdTechIdentifier}. Otherwise, {@link IllegalStateException}
+ * will be thrown.
+ * <li>{@code List of ad selection ids} should exist and come from {@link
+ * AdSelectionManager#selectAds} calls originated from the same application. Otherwise,
+ * {@link IllegalArgumentException} for input validation will raise listing violating ad
+ * selection ids.
+ * <li>{@code Selection logic URI} that could follow either the HTTPS or Ad Selection Prebuilt
+ * schemas.
+ * <p>If the URI follows HTTPS schema then the host should match the {@code seller}.
+ * Otherwise, {@link IllegalArgumentException} will be thrown.
+ * <p>Prebuilt URIs are a way of substituting a generic pre-built logics for the required
+ * JavaScripts for {@code selectOutcome}. Prebuilt Uri for this endpoint should follow;
+ * <ul>
+ * <li>{@code
+ * ad-selection-prebuilt://ad-selection-from-outcomes/<name>?<script-generation-parameters>}
+ * </ul>
+ * <p>If an unsupported prebuilt URI is passed or prebuilt URI feature is disabled by the
+ * service then {@link IllegalArgumentException} will be thrown.
+ * <p>See {@link AdSelectionFromOutcomesConfig.Builder#setSelectionLogicUri} for supported
+ * {@code <name>} and required {@code <script-generation-parameters>}.
+ * </ul>
+ *
+ * <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
+ * the API received to run the ad selection.
+ *
+ * <p>If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection
+ * services.", it is caused by an internal failure of the ad selection service.
+ *
+ * <p>If the {@link TimeoutException} is thrown, it is caused when a timeout is encountered
+ * during bidding, scoring, or overall selection process to find winning Ad.
+ *
+ * <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
+ * exceeds the allowed rate limits and is throttled.
+ *
+ * <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
+ * or permission is not requested.
+ *
+ * @hide
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void selectAds(
+ @NonNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<AdSelectionOutcome, Exception> receiver) {
+ Objects.requireNonNull(adSelectionFromOutcomesConfig);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = getService();
+ service.selectAdsFromOutcomes(
+ new AdSelectionFromOutcomesInput.Builder()
+ .setAdSelectionFromOutcomesConfig(adSelectionFromOutcomesConfig)
+ .setCallerPackageName(getCallerPackageName())
+ .build(),
+ new CallerMetadata.Builder()
+ .setBinderElapsedTimestamp(SystemClock.elapsedRealtime())
+ .build(),
+ new AdSelectionCallback.Stub() {
+ @Override
+ public void onSuccess(AdSelectionResponse resultParcel) {
+ executor.execute(
+ () -> {
+ if (resultParcel == null) {
+ receiver.onResult(AdSelectionOutcome.NO_OUTCOME);
+ } else {
+ receiver.onResult(
+ new AdSelectionOutcome.Builder()
+ .setAdSelectionId(
+ resultParcel.getAdSelectionId())
+ .setRenderUri(
+ resultParcel.getRenderUri())
+ .build());
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () -> {
+ receiver.onError(
+ AdServicesStatusUtils.asException(failureParcel));
+ });
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Failure of AdSelection service.");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Notifies the service that there is a new impression to report for the ad selected by the
+ * ad-selection run identified by {@code adSelectionId}. There is no guarantee about when the
+ * impression will be reported. The impression reporting could be delayed and reports could be
+ * batched.
+ *
+ * <p>To calculate the winning seller reporting URL, the service fetches the seller's JavaScript
+ * logic from the {@link AdSelectionConfig#getDecisionLogicUri()} found at {@link
+ * ReportImpressionRequest#getAdSelectionConfig()}. Then, the service executes one of the
+ * functions found in the seller JS called {@code reportResult}, providing on-device signals as
+ * well as {@link ReportImpressionRequest#getAdSelectionConfig()} as input parameters.
+ *
+ * <p>The function definition of {@code reportResult} is:
+ *
+ * <p>{@code function reportResult(ad_selection_config, render_url, bid, contextual_signals) {
+ * return { 'status': status, 'results': {'signals_for_buyer': signals_for_buyer,
+ * 'reporting_url': reporting_url } }; } }
+ *
+ * <p>To calculate the winning buyer reporting URL, the service fetches the winning buyer's
+ * JavaScript logic which is fetched via the buyer's {@link
+ * android.adservices.customaudience.CustomAudience#getBiddingLogicUri()}. Then, the service
+ * executes one of the functions found in the buyer JS called {@code reportWin}, providing
+ * on-device signals, {@code signals_for_buyer} calculated by {@code reportResult}, and specific
+ * fields from {@link ReportImpressionRequest#getAdSelectionConfig()} as input parameters.
+ *
+ * <p>The function definition of {@code reportWin} is:
+ *
+ * <p>{@code function reportWin(ad_selection_signals, per_buyer_signals, signals_for_buyer,
+ * contextual_signals, custom_audience_reporting_signals) { return {'status': 0, 'results':
+ * {'reporting_url': reporting_url } }; } }
+ *
+ * <p>The output is passed by the {@code receiver}, which either returns an empty {@link Object}
+ * for a successful run, or an {@link Exception} includes the type of the exception thrown and
+ * the corresponding error message.
+ *
+ * <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
+ * the API received to report the impression.
+ *
+ * <p>If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection
+ * services.", it is caused by an internal failure of the ad selection service.
+ *
+ * <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
+ * exceeds the allowed rate limits and is throttled.
+ *
+ * <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
+ * or permission is not requested.
+ *
+ * <p>Impressions will be reported at most once as a best-effort attempt.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void reportImpression(
+ @NonNull ReportImpressionRequest request,
+ @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = getService();
+ service.reportImpression(
+ new ReportImpressionInput.Builder()
+ .setAdSelectionId(request.getAdSelectionId())
+ .setAdSelectionConfig(request.getAdSelectionConfig())
+ .setCallerPackageName(getCallerPackageName())
+ .build(),
+ new ReportImpressionCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () -> {
+ receiver.onError(
+ AdServicesStatusUtils.asException(failureParcel));
+ });
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Notifies PPAPI that there is a new interaction to report for the ad selected by the
+ * ad-selection run identified by {@code adSelectionId}. There is no guarantee about when the
+ * interaction will be reported. The interaction reporting could be delayed and interactions
+ * could be batched.
+ *
+ * <p>The output is passed by the receiver, which either returns an empty {@link Object} for a
+ * successful run, or an {@link Exception} includes the type of the exception thrown and the
+ * corresponding error message.
+ *
+ * <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
+ * the API received to report the interaction.
+ *
+ * <p>If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection
+ * services.", it is caused by an internal failure of the ad selection service.
+ *
+ * <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
+ * exceeds the allowed rate limits and is throttled.
+ *
+ * <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
+ * or permission is not requested.
+ *
+ * <p>Interactions will be reported at most once as a best-effort attempt.
+ *
+ * @hide
+ */
+ // TODO(b/261812140): Unhide for report interaction API review
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void reportInteraction(
+ @NonNull ReportInteractionRequest request,
+ @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = getService();
+ service.reportInteraction(
+ new ReportInteractionInput.Builder()
+ .setAdSelectionId(request.getAdSelectionId())
+ .setInteractionKey(request.getInteractionKey())
+ .setInteractionData(request.getInteractionData())
+ .setReportingDestinations(request.getReportingDestinations())
+ .setCallerPackageName(getCallerPackageName())
+ .build(),
+ new ReportInteractionCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () -> {
+ receiver.onError(
+ AdServicesStatusUtils.asException(failureParcel));
+ });
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Gives the provided list of adtechs the ability to do app install filtering on the calling
+ * app.
+ *
+ * <p>The input {@code request} is provided by the Ads SDK and the {@code request} object is
+ * transferred via a Binder call. For this reason, the total size of these objects is bound to
+ * the Android IPC limitations. Failures to transfer the {@code advertisers} will throws an
+ * {@link TransactionTooLargeException}.
+ *
+ * <p>The output is passed by the receiver, which either returns an empty {@link Object} for a
+ * successful run, or an {@link Exception} includes the type of the exception thrown and the
+ * corresponding error message.
+ *
+ * <p>If the {@link IllegalArgumentException} is thrown, it is caused by invalid input argument
+ * the API received.
+ *
+ * <p>If the {@link IllegalStateException} is thrown with error message "Failure of AdSelection
+ * services.", it is caused by an internal failure of the ad selection service.
+ *
+ * <p>If the {@link LimitExceededException} is thrown, it is caused when the calling package
+ * exceeds the allowed rate limits and is throttled.
+ *
+ * <p>If the {@link SecurityException} is thrown, it is caused when the caller is not authorized
+ * or permission is not requested.
+ *
+ * @hide
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void setAppInstallAdvertisers(
+ @NonNull SetAppInstallAdvertisersRequest request,
+ @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = getService();
+ service.setAppInstallAdvertisers(
+ new SetAppInstallAdvertisersInput.Builder()
+ .setAdvertisers(request.getAdvertisers())
+ .setCallerPackageName(getCallerPackageName())
+ .build(),
+ new SetAppInstallAdvertisersCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () -> {
+ receiver.onError(
+ AdServicesStatusUtils.asException(failureParcel));
+ });
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Updates the counter histograms for an ad which was previously selected by a call to {@link
+ * #selectAds(AdSelectionConfig, Executor, OutcomeReceiver)}.
+ *
+ * <p>The counter histograms are used in ad selection to inform frequency cap filtering on
+ * candidate ads, where ads whose frequency caps are met or exceeded are removed from the
+ * bidding process during ad selection.
+ *
+ * <p>Counter histograms can only be updated for ads specified by the given {@code
+ * adSelectionId} returned by a recent call to FLEDGE ad selection from the same caller app.
+ *
+ * <p>A {@link SecurityException} is returned via the {@code outcomeReceiver} if:
+ *
+ * <ol>
+ * <li>the app has not declared the correct permissions in its manifest, or
+ * <li>the app or entity identified by the {@code callerAdTechIdentifier} are not authorized
+ * to use the API.
+ * </ol>
+ *
+ * An {@link IllegalStateException} is returned via the {@code outcomeReceiver} if the call does
+ * not come from an app with a foreground activity.
+ *
+ * <p>A {@link LimitExceededException} is returned via the {@code outcomeReceiver} if the call
+ * exceeds the calling app's API throttle.
+ *
+ * <p>In all other failure cases, the {@code outcomeReceiver} will return an empty {@link
+ * Object}. Note that to protect user privacy, internal errors will not be sent back via an
+ * exception.
+ *
+ * @hide
+ */
+ // TODO(b/221876775): Unhide for frequency cap API review
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void updateAdCounterHistogram(
+ @NonNull UpdateAdCounterHistogramRequest updateAdCounterHistogramRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> outcomeReceiver) {
+ Objects.requireNonNull(updateAdCounterHistogramRequest, "Request must not be null");
+ Objects.requireNonNull(executor, "Executor must not be null");
+ Objects.requireNonNull(outcomeReceiver, "Outcome receiver must not be null");
+
+ try {
+ final AdSelectionService service = Objects.requireNonNull(getService());
+ service.updateAdCounterHistogram(
+ new UpdateAdCounterHistogramInput.Builder()
+ .setAdEventType(updateAdCounterHistogramRequest.getAdEventType())
+ .setAdSelectionId(updateAdCounterHistogramRequest.getAdSelectionId())
+ .setCallerAdTech(updateAdCounterHistogramRequest.getCallerAdTech())
+ .setCallerPackageName(getCallerPackageName())
+ .build(),
+ new UpdateAdCounterHistogramCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> outcomeReceiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () -> {
+ outcomeReceiver.onError(
+ AdServicesStatusUtils.asException(failureParcel));
+ });
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service");
+ outcomeReceiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Remote exception encountered while updating ad counter histogram");
+ outcomeReceiver.onError(new IllegalStateException("Failure of AdSelection service", e));
+ }
+ }
+
+ private String getCallerPackageName() {
+ SandboxedSdkContext sandboxedSdkContext =
+ SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
+ return sandboxedSdkContext == null
+ ? mContext.getPackageName()
+ : sandboxedSdkContext.getClientPackageName();
+ }
+}
diff --git a/android-34/android/adservices/adselection/AdSelectionOutcome.java b/android-34/android/adservices/adselection/AdSelectionOutcome.java
new file mode 100644
index 0000000..326134f
--- /dev/null
+++ b/android-34/android/adservices/adselection/AdSelectionOutcome.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * This class represents a field in the {@code OutcomeReceiver}, which is an input to the {@link
+ * AdSelectionManager#selectAds} in the {@link AdSelectionManager}. This field is populated in the
+ * case of a successful {@link AdSelectionManager#selectAds} call.
+ *
+ */
+public class AdSelectionOutcome {
+ /**
+ * Represents an AdSelectionOutcome with empty results.
+ *
+ * @hide
+ */
+ @NonNull public static final AdSelectionOutcome NO_OUTCOME = new AdSelectionOutcome();
+
+ /** @hide */
+ public static final String UNSET_AD_SELECTION_ID_MESSAGE = "Ad selection ID must be set";
+
+ /** @hide */
+ public static final int UNSET_AD_SELECTION_ID = 0;
+
+ private final long mAdSelectionId;
+ @NonNull private final Uri mRenderUri;
+
+ private AdSelectionOutcome() {
+ mAdSelectionId = UNSET_AD_SELECTION_ID;
+ mRenderUri = Uri.EMPTY;
+ }
+
+ private AdSelectionOutcome(long adSelectionId, @NonNull Uri renderUri) {
+ Objects.requireNonNull(renderUri);
+
+ mAdSelectionId = adSelectionId;
+ mRenderUri = renderUri;
+ }
+
+ /** Returns the renderUri that the AdSelection returns. */
+ @NonNull
+ public Uri getRenderUri() {
+ return mRenderUri;
+ }
+
+ /** Returns the adSelectionId that identifies the AdSelection. */
+ @NonNull
+ public long getAdSelectionId() {
+ return mAdSelectionId;
+ }
+
+ /**
+ * Returns whether the outcome contains results or empty. Empty outcomes' {@code render uris}
+ * shouldn't be used.
+ *
+ * @hide
+ */
+ public boolean hasOutcome() {
+ return !this.equals(NO_OUTCOME);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof AdSelectionOutcome) {
+ AdSelectionOutcome adSelectionOutcome = (AdSelectionOutcome) o;
+ return mAdSelectionId == adSelectionOutcome.mAdSelectionId
+ && Objects.equals(mRenderUri, adSelectionOutcome.mRenderUri);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAdSelectionId, mRenderUri);
+ }
+
+ /**
+ * Builder for {@link AdSelectionOutcome} objects.
+ */
+ public static final class Builder {
+ private long mAdSelectionId = UNSET_AD_SELECTION_ID;
+ @Nullable private Uri mRenderUri;
+
+ public Builder() {}
+
+ /** Sets the mAdSelectionId. */
+ @NonNull
+ public AdSelectionOutcome.Builder setAdSelectionId(long adSelectionId) {
+ this.mAdSelectionId = adSelectionId;
+ return this;
+ }
+
+ /** Sets the RenderUri. */
+ @NonNull
+ public AdSelectionOutcome.Builder setRenderUri(@NonNull Uri renderUri) {
+ Objects.requireNonNull(renderUri);
+
+ mRenderUri = renderUri;
+ return this;
+ }
+
+ /**
+ * Builds a {@link AdSelectionOutcome} instance.
+ *
+ * @throws IllegalArgumentException if the adSelectionIid is not set
+ * @throws NullPointerException if the RenderUri is null
+ */
+ @NonNull
+ public AdSelectionOutcome build() {
+ Objects.requireNonNull(mRenderUri);
+
+ Preconditions.checkArgument(
+ mAdSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
+
+ return new AdSelectionOutcome(mAdSelectionId, mRenderUri);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/AdSelectionResponse.java b/android-34/android/adservices/adselection/AdSelectionResponse.java
new file mode 100644
index 0000000..c1a0095
--- /dev/null
+++ b/android-34/android/adservices/adselection/AdSelectionResponse.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID;
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID_MESSAGE;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * This class represents the response returned by the {@link AdSelectionManager} as the result of a
+ * successful {@code selectAds} call.
+ *
+ * @hide
+ */
+public final class AdSelectionResponse implements Parcelable {
+ private final long mAdSelectionId;
+ @NonNull private final Uri mRenderUri;
+
+ private AdSelectionResponse(long adSelectionId, @NonNull Uri renderUri) {
+ Objects.requireNonNull(renderUri);
+
+ mAdSelectionId = adSelectionId;
+ mRenderUri = renderUri;
+ }
+
+ private AdSelectionResponse(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mAdSelectionId = in.readLong();
+ mRenderUri = Uri.CREATOR.createFromParcel(in);
+ }
+
+ @NonNull
+ public static final Creator<AdSelectionResponse> CREATOR =
+ new Parcelable.Creator<AdSelectionResponse>() {
+ @Override
+ public AdSelectionResponse createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new AdSelectionResponse(in);
+ }
+
+ @Override
+ public AdSelectionResponse[] newArray(int size) {
+ return new AdSelectionResponse[size];
+ }
+ };
+
+ /** Returns the renderUri that the AdSelection returns. */
+ @NonNull
+ public Uri getRenderUri() {
+ return mRenderUri;
+ }
+
+ /** Returns the adSelectionId that identifies the AdSelection. */
+ @NonNull
+ public long getAdSelectionId() {
+ return mAdSelectionId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof AdSelectionResponse) {
+ AdSelectionResponse adSelectionResponse = (AdSelectionResponse) o;
+ return mAdSelectionId == adSelectionResponse.mAdSelectionId
+ && Objects.equals(mRenderUri, adSelectionResponse.mRenderUri);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAdSelectionId, mRenderUri);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ dest.writeLong(mAdSelectionId);
+ mRenderUri.writeToParcel(dest, flags);
+ }
+
+ @Override
+ public String toString() {
+ return "AdSelectionResponse{"
+ + "mAdSelectionId="
+ + mAdSelectionId
+ + ", mRenderUri="
+ + mRenderUri
+ + '}';
+ }
+
+ /**
+ * Builder for {@link AdSelectionResponse} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ private long mAdSelectionId = UNSET_AD_SELECTION_ID;
+ @NonNull private Uri mRenderUri;
+
+ public Builder() {}
+
+ /** Sets the mAdSelectionId. */
+ @NonNull
+ public AdSelectionResponse.Builder setAdSelectionId(long adSelectionId) {
+ this.mAdSelectionId = adSelectionId;
+ return this;
+ }
+
+ /** Sets the RenderUri. */
+ @NonNull
+ public AdSelectionResponse.Builder setRenderUri(@NonNull Uri renderUri) {
+ Objects.requireNonNull(renderUri);
+
+ mRenderUri = renderUri;
+ return this;
+ }
+
+ /**
+ * Builds a {@link AdSelectionResponse} instance.
+ *
+ * @throws IllegalArgumentException if the adSelectionIid is not set
+ * @throws NullPointerException if the RenderUri is null
+ */
+ @NonNull
+ public AdSelectionResponse build() {
+ Objects.requireNonNull(mRenderUri);
+
+ Preconditions.checkArgument(
+ mAdSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
+
+ return new AdSelectionResponse(mAdSelectionId, mRenderUri);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/AdWithBid.java b/android-34/android/adservices/adselection/AdWithBid.java
new file mode 100644
index 0000000..40d9513
--- /dev/null
+++ b/android-34/android/adservices/adselection/AdWithBid.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.adservices.common.AdData;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represents an ad and its corresponding bid value after the bid generation step in the ad
+ * selection process.
+ *
+ * <p>The ads and their bids are fed into an ad scoring process which will inform the final ad
+ * selection. The currency unit for the bid is expected to be the same requested by the seller when
+ * initiating the selection process and not specified in this class. The seller can provide the
+ * currency via AdSelectionSignals. The currency is opaque to FLEDGE for now.
+ *
+ * @hide
+ */
+public final class AdWithBid implements Parcelable {
+ @NonNull
+ private final AdData mAdData;
+ private final double mBid;
+
+ @NonNull
+ public static final Creator<AdWithBid> CREATOR =
+ new Creator<AdWithBid>() {
+ @Override
+ public AdWithBid createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ return new AdWithBid(in);
+ }
+
+ @Override
+ public AdWithBid[] newArray(int size) {
+ return new AdWithBid[size];
+ }
+ };
+
+ /**
+ * @param adData An {@link AdData} object defining an ad's render URI and buyer metadata
+ * @param bid The amount of money a buyer has bid to show an ad; note that while the bid is
+ * expected to be non-negative, this is only enforced during the ad selection process
+ * @throws NullPointerException if adData is null
+ */
+ public AdWithBid(@NonNull AdData adData, double bid) {
+ Objects.requireNonNull(adData);
+ mAdData = adData;
+ mBid = bid;
+ }
+
+ private AdWithBid(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ mAdData = AdData.CREATOR.createFromParcel(in);
+ mBid = in.readDouble();
+ }
+
+ /**
+ * @return the ad that was bid on
+ */
+ @NonNull
+ public AdData getAdData() {
+ return mAdData;
+ }
+
+ /**
+ * The bid is the amount of money an advertiser has bid during the ad selection process to show
+ * an ad. The bid could be any non-negative {@code double}, such as 0.00, 0.17, 1.10, or
+ * 1000.00.
+ *
+ * <p>The currency for a bid would be controlled by Seller and will remain consistent across a
+ * run of Ad selection. This could be achieved by leveraging bidding signals during
+ * "generateBid()" phase and using the same currency during the creation of contextual ads.
+ * Having currency unit as a dedicated field could be supported in future releases.
+ *
+ * @return the bid value to be passed to the scoring function when scoring the ad returned by
+ * {@link #getAdData()}
+ */
+ public double getBid() {
+ return mBid;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ mAdData.writeToParcel(dest, flags);
+ dest.writeDouble(mBid);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AdWithBid)) return false;
+ AdWithBid adWithBid = (AdWithBid) o;
+ return Double.compare(adWithBid.mBid, mBid) == 0
+ && Objects.equals(mAdData, adWithBid.mAdData);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAdData, mBid);
+ }
+}
diff --git a/android-34/android/adservices/adselection/AddAdSelectionFromOutcomesOverrideRequest.java b/android-34/android/adservices/adselection/AddAdSelectionFromOutcomesOverrideRequest.java
new file mode 100644
index 0000000..027aa59
--- /dev/null
+++ b/android-34/android/adservices/adselection/AddAdSelectionFromOutcomesOverrideRequest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.adservices.common.AdSelectionSignals;
+import android.annotation.NonNull;
+
+import java.util.Objects;
+
+/**
+ * This POJO represents the {@link
+ * TestAdSelectionManager#overrideAdSelectionFromOutcomesConfigRemoteInfo} (
+ * AddAdSelectionOverrideRequest, Executor, OutcomeReceiver)} request
+ *
+ * <p>It contains, a {@link AdSelectionFromOutcomesConfig} which will serve as the identifier for
+ * the specific override, a {@code String} selectionLogicJs and {@code String} selectionSignals
+ * field representing the override value
+ *
+ * @hide
+ */
+public class AddAdSelectionFromOutcomesOverrideRequest {
+ @NonNull private final AdSelectionFromOutcomesConfig mAdSelectionFromOutcomesConfig;
+
+ @NonNull private final String mOutcomeSelectionLogicJs;
+
+ @NonNull private final AdSelectionSignals mOutcomeSelectionTrustedSignals;
+
+ /** Builds a {@link AddAdSelectionFromOutcomesOverrideRequest} instance. */
+ public AddAdSelectionFromOutcomesOverrideRequest(
+ @NonNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig,
+ @NonNull String outcomeSelectionLogicJs,
+ @NonNull AdSelectionSignals outcomeSelectionTrustedSignals) {
+ Objects.requireNonNull(adSelectionFromOutcomesConfig);
+ Objects.requireNonNull(outcomeSelectionLogicJs);
+ Objects.requireNonNull(outcomeSelectionTrustedSignals);
+
+ mAdSelectionFromOutcomesConfig = adSelectionFromOutcomesConfig;
+ mOutcomeSelectionLogicJs = outcomeSelectionLogicJs;
+ mOutcomeSelectionTrustedSignals = outcomeSelectionTrustedSignals;
+ }
+
+ /**
+ * @return an instance of {@link AdSelectionFromOutcomesConfig}, the configuration of the ad
+ * selection process. This configuration provides the data necessary to run Ad Selection
+ * flow that generates bids and scores to find a wining ad for rendering.
+ */
+ @NonNull
+ public AdSelectionFromOutcomesConfig getAdSelectionFromOutcomesConfig() {
+ return mAdSelectionFromOutcomesConfig;
+ }
+
+ /**
+ * @return The override javascript result, should be a string that contains valid JS code. The
+ * code should contain the outcome selection logic that will be executed during ad outcome
+ * selection.
+ */
+ @NonNull
+ public String getOutcomeSelectionLogicJs() {
+ return mOutcomeSelectionLogicJs;
+ }
+
+ /**
+ * @return The override trusted scoring signals, should be a valid json string. The trusted
+ * signals would be fed into the outcome selection logic during ad outcome selection.
+ */
+ @NonNull
+ public AdSelectionSignals getOutcomeSelectionTrustedSignals() {
+ return mOutcomeSelectionTrustedSignals;
+ }
+}
diff --git a/android-34/android/adservices/adselection/AddAdSelectionOverrideRequest.java b/android-34/android/adservices/adselection/AddAdSelectionOverrideRequest.java
new file mode 100644
index 0000000..a8f7819
--- /dev/null
+++ b/android-34/android/adservices/adselection/AddAdSelectionOverrideRequest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.adservices.common.AdSelectionSignals;
+import android.annotation.NonNull;
+import android.os.OutcomeReceiver;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This POJO represents the {@link
+ * TestAdSelectionManager#overrideAdSelectionConfigRemoteInfo(AddAdSelectionOverrideRequest,
+ * Executor, OutcomeReceiver)} request
+ *
+ * <p>It contains, a {@link AdSelectionConfig} which will serve as the identifier for the specific
+ * override, a {@code String} decisionLogicJs and {@code String} trustedScoringSignals field
+ * representing the override value
+ */
+public class AddAdSelectionOverrideRequest {
+ @NonNull private final AdSelectionConfig mAdSelectionConfig;
+
+ @NonNull private final String mDecisionLogicJs;
+
+ @NonNull private final AdSelectionSignals mTrustedScoringSignals;
+
+ @NonNull private final BuyersDecisionLogic mBuyersDecisionLogic;
+
+ /**
+ * Builds a {@link AddAdSelectionOverrideRequest} instance.
+ *
+ * @hide
+ */
+ public AddAdSelectionOverrideRequest(
+ @NonNull AdSelectionConfig adSelectionConfig,
+ @NonNull String decisionLogicJs,
+ @NonNull AdSelectionSignals trustedScoringSignals,
+ @NonNull BuyersDecisionLogic buyersDecisionLogic) {
+ Objects.requireNonNull(adSelectionConfig);
+ Objects.requireNonNull(decisionLogicJs);
+ Objects.requireNonNull(trustedScoringSignals);
+ Objects.requireNonNull(buyersDecisionLogic);
+
+ mAdSelectionConfig = adSelectionConfig;
+ mDecisionLogicJs = decisionLogicJs;
+ mTrustedScoringSignals = trustedScoringSignals;
+ mBuyersDecisionLogic = buyersDecisionLogic;
+ }
+
+ public AddAdSelectionOverrideRequest(
+ @NonNull AdSelectionConfig adSelectionConfig,
+ @NonNull String decisionLogicJs,
+ @NonNull AdSelectionSignals trustedScoringSignals) {
+ this(adSelectionConfig, decisionLogicJs, trustedScoringSignals, BuyersDecisionLogic.EMPTY);
+ }
+
+ /**
+ * @return an instance of {@link AdSelectionConfig}, the configuration of the ad selection
+ * process. This configuration provides the data necessary to run Ad Selection flow that
+ * generates bids and scores to find a wining ad for rendering.
+ */
+ @NonNull
+ public AdSelectionConfig getAdSelectionConfig() {
+ return mAdSelectionConfig;
+ }
+
+ /**
+ * @return The override javascript result, should be a string that contains valid JS code. The
+ * code should contain the scoring logic that will be executed during Ad selection.
+ */
+ @NonNull
+ public String getDecisionLogicJs() {
+ return mDecisionLogicJs;
+ }
+
+ /**
+ * @return The override trusted scoring signals, should be a valid json string. The trusted
+ * signals would be fed into the scoring logic during Ad Selection.
+ */
+ @NonNull
+ public AdSelectionSignals getTrustedScoringSignals() {
+ return mTrustedScoringSignals;
+ }
+
+ /**
+ * @return The override for the decision logic for each buyer that is used by contextual ads for
+ * reporting, which may be extended to updating bid values for contextual ads in the future
+ * @hide
+ */
+ @NonNull
+ public BuyersDecisionLogic getBuyersDecisionLogic() {
+ return mBuyersDecisionLogic;
+ }
+}
diff --git a/android-34/android/adservices/adselection/BuyersDecisionLogic.java b/android-34/android/adservices/adselection/BuyersDecisionLogic.java
new file mode 100644
index 0000000..ba42a16
--- /dev/null
+++ b/android-34/android/adservices/adselection/BuyersDecisionLogic.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @return The override for the decision logic for each buyer that is used by contextual ads for
+ * reporting, which may be extended to updating bid values for contextual ads in the future
+ * @hide
+ */
+public final class BuyersDecisionLogic implements Parcelable {
+
+ @NonNull
+ public static final BuyersDecisionLogic EMPTY = new BuyersDecisionLogic(Collections.emptyMap());
+
+ @NonNull private Map<AdTechIdentifier, DecisionLogic> mLogicMap;
+
+ public BuyersDecisionLogic(@NonNull Map<AdTechIdentifier, DecisionLogic> logicMap) {
+ Objects.requireNonNull(logicMap);
+ mLogicMap = logicMap;
+ }
+
+ private BuyersDecisionLogic(@NonNull Parcel in) {
+ mLogicMap =
+ AdServicesParcelableUtil.readMapFromParcel(
+ in, AdTechIdentifier::fromString, DecisionLogic.class);
+ }
+
+ @NonNull
+ public static final Creator<BuyersDecisionLogic> CREATOR =
+ new Creator<BuyersDecisionLogic>() {
+ @Override
+ public BuyersDecisionLogic createFromParcel(Parcel in) {
+ Objects.requireNonNull(in);
+ return new BuyersDecisionLogic(in);
+ }
+
+ @Override
+ public BuyersDecisionLogic[] newArray(int size) {
+ return new BuyersDecisionLogic[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ AdServicesParcelableUtil.writeMapToParcel(dest, mLogicMap);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mLogicMap);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BuyersDecisionLogic)) return false;
+ BuyersDecisionLogic logicMap = (BuyersDecisionLogic) o;
+ return mLogicMap.equals(logicMap.getLogicMap());
+ }
+
+ @NonNull
+ public Map<AdTechIdentifier, DecisionLogic> getLogicMap() {
+ return mLogicMap;
+ }
+}
diff --git a/android-34/android/adservices/adselection/ContextualAds.java b/android-34/android/adservices/adselection/ContextualAds.java
new file mode 100644
index 0000000..c34056d
--- /dev/null
+++ b/android-34/android/adservices/adselection/ContextualAds.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Contains Ads supplied by Seller for the Contextual Path
+ *
+ * <p>Instances of this class are created by SDKs to be injected as part of {@link
+ * AdSelectionConfig} and passed to {@link AdSelectionManager#selectAds}
+ *
+ * @hide
+ */
+public final class ContextualAds implements Parcelable {
+ @NonNull private final AdTechIdentifier mBuyer;
+ @NonNull private final Uri mDecisionLogicUri;
+ @NonNull private final List<AdWithBid> mAdsWithBid;
+
+ @NonNull
+ public static final Creator<ContextualAds> CREATOR =
+ new Creator<ContextualAds>() {
+ @Override
+ public ContextualAds createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new ContextualAds(in);
+ }
+
+ @Override
+ public ContextualAds[] newArray(int size) {
+ return new ContextualAds[0];
+ }
+ };
+
+ private ContextualAds(
+ @NonNull AdTechIdentifier buyer,
+ @NonNull Uri decisionLogicUri,
+ @NonNull List<AdWithBid> adsWithBid) {
+ this.mBuyer = buyer;
+ this.mDecisionLogicUri = decisionLogicUri;
+ this.mAdsWithBid = adsWithBid;
+ }
+
+ private ContextualAds(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ mBuyer = AdTechIdentifier.CREATOR.createFromParcel(in);
+ mDecisionLogicUri = Uri.CREATOR.createFromParcel(in);
+ mAdsWithBid = in.createTypedArrayList(AdWithBid.CREATOR);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ mBuyer.writeToParcel(dest, flags);
+ mDecisionLogicUri.writeToParcel(dest, flags);
+ dest.writeTypedList(mAdsWithBid);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ContextualAds)) return false;
+ ContextualAds that = (ContextualAds) o;
+ return Objects.equals(mBuyer, that.mBuyer)
+ && Objects.equals(mDecisionLogicUri, that.mDecisionLogicUri)
+ && Objects.equals(mAdsWithBid, that.mAdsWithBid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mBuyer, mDecisionLogicUri, mAdsWithBid);
+ }
+
+ /** @return the Ad tech identifier from which this contextual Ad would have been downloaded */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ /**
+ * @return the URI used for to retrieve the updateBid() and reportWin() function used during the
+ * ad selection and reporting process
+ */
+ @NonNull
+ public Uri getDecisionLogicUri() {
+ return mDecisionLogicUri;
+ }
+
+ /** @return the Ad data with bid value associated with this ad */
+ @NonNull
+ public List<AdWithBid> getAdsWithBid() {
+ return mAdsWithBid;
+ }
+
+ /** Builder for {@link ContextualAds} object */
+ public static final class Builder {
+ @Nullable private AdTechIdentifier mBuyer;
+ @Nullable private Uri mDecisionLogicUri;
+ @Nullable private List<AdWithBid> mAdsWithBid;
+
+ public Builder() {}
+
+ /**
+ * Sets the buyer Ad tech Identifier
+ *
+ * <p>See {@link #getBuyer()} for more details
+ */
+ @NonNull
+ public ContextualAds.Builder setBuyer(@NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer);
+
+ this.mBuyer = buyer;
+ return this;
+ }
+
+ /**
+ * Sets the URI to fetch the decision logic used in ad selection and reporting
+ *
+ * <p>See {@link #getDecisionLogicUri()} for more details
+ */
+ @NonNull
+ public ContextualAds.Builder setDecisionLogicUri(@NonNull Uri decisionLogicUri) {
+ Objects.requireNonNull(decisionLogicUri);
+
+ this.mDecisionLogicUri = decisionLogicUri;
+ return this;
+ }
+
+ /**
+ * Sets the Ads with pre-defined bid values
+ *
+ * <p>See {@link #getAdsWithBid()} for more details
+ */
+ @NonNull
+ public ContextualAds.Builder setAdsWithBid(@NonNull List<AdWithBid> adsWithBid) {
+ Objects.requireNonNull(adsWithBid);
+
+ this.mAdsWithBid = adsWithBid;
+ return this;
+ }
+
+ /**
+ * Builds a {@link ContextualAds} instance.
+ *
+ * @throws NullPointerException if any required params are null
+ */
+ @NonNull
+ public ContextualAds build() {
+ Objects.requireNonNull(mBuyer);
+ Objects.requireNonNull(mDecisionLogicUri);
+ Objects.requireNonNull(mAdsWithBid);
+ return new ContextualAds(mBuyer, mDecisionLogicUri, mAdsWithBid);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/DecisionLogic.java b/android-34/android/adservices/adselection/DecisionLogic.java
new file mode 100644
index 0000000..5e53b24
--- /dev/null
+++ b/android-34/android/adservices/adselection/DecisionLogic.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Generic Decision logic that could be provided by the buyer or seller.
+ *
+ * @hide
+ */
+public final class DecisionLogic implements Parcelable {
+
+ @NonNull private String mDecisionLogic;
+
+ public DecisionLogic(@NonNull String buyerDecisionLogic) {
+ Objects.requireNonNull(buyerDecisionLogic);
+ mDecisionLogic = buyerDecisionLogic;
+ }
+
+ private DecisionLogic(@NonNull Parcel in) {
+ this(in.readString());
+ }
+
+ @NonNull
+ public static final Creator<DecisionLogic> CREATOR =
+ new Creator<DecisionLogic>() {
+ @Override
+ public DecisionLogic createFromParcel(Parcel in) {
+ Objects.requireNonNull(in);
+ return new DecisionLogic(in);
+ }
+
+ @Override
+ public DecisionLogic[] newArray(int size) {
+ return new DecisionLogic[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ dest.writeString(mDecisionLogic);
+ }
+
+ @Override
+ public String toString() {
+ return mDecisionLogic;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDecisionLogic);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DecisionLogic)) return false;
+ DecisionLogic decisionLogic = (DecisionLogic) o;
+ return mDecisionLogic.equals(decisionLogic.getLogic());
+ }
+
+ @NonNull
+ public String getLogic() {
+ return mDecisionLogic;
+ }
+}
diff --git a/android-34/android/adservices/adselection/RemoveAdCounterHistogramOverrideInput.java b/android-34/android/adservices/adselection/RemoveAdCounterHistogramOverrideInput.java
new file mode 100644
index 0000000..43bfa87
--- /dev/null
+++ b/android-34/android/adservices/adselection/RemoveAdCounterHistogramOverrideInput.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_AD_COUNTER_KEY_MESSAGE;
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_BUYER_MESSAGE;
+import static android.adservices.adselection.UpdateAdCounterHistogramRequest.UNSET_AD_EVENT_TYPE_MESSAGE;
+import static android.adservices.common.FrequencyCapFilters.AD_EVENT_TYPE_INVALID;
+
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.FrequencyCapFilters;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Input object for removing ad counter histogram overrides.
+ *
+ * <p>Histogram overrides replace actual ad counter histograms used in ad selection. Overrides may
+ * only be set in debuggable apps on phones running a debuggable OS build with developer options
+ * enabled. Overrides are only available from the calling app.
+ *
+ * @hide
+ */
+public final class RemoveAdCounterHistogramOverrideInput implements Parcelable {
+ @FrequencyCapFilters.AdEventType private final int mAdEventType;
+ @NonNull private final String mAdCounterKey;
+ @NonNull private final AdTechIdentifier mBuyer;
+
+ @NonNull
+ public static final Creator<RemoveAdCounterHistogramOverrideInput> CREATOR =
+ new Creator<RemoveAdCounterHistogramOverrideInput>() {
+ @Override
+ public RemoveAdCounterHistogramOverrideInput createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ return new RemoveAdCounterHistogramOverrideInput(in);
+ }
+
+ @Override
+ public RemoveAdCounterHistogramOverrideInput[] newArray(int size) {
+ return new RemoveAdCounterHistogramOverrideInput[size];
+ }
+ };
+
+ private RemoveAdCounterHistogramOverrideInput(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mAdEventType = builder.mAdEventType;
+ mAdCounterKey = builder.mAdCounterKey;
+ mBuyer = builder.mBuyer;
+ }
+
+ private RemoveAdCounterHistogramOverrideInput(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mAdEventType = in.readInt();
+ mAdCounterKey = in.readString();
+ mBuyer = AdTechIdentifier.fromString(in.readString());
+ }
+
+ /**
+ * Gets the {@link FrequencyCapFilters.AdEventType} for the ad counter histogram override.
+ *
+ * <p>The ad event type is used with the ad counter key from {@link #getAdCounterKey()} and the
+ * buyer adtech from {@link #getBuyer()} to specify which histogram to use in ad selection
+ * filtering. The ad event type would normally be specified by an app/SDK after a
+ * FLEDGE-selected ad is rendered.
+ */
+ @FrequencyCapFilters.AdEventType
+ public int getAdEventType() {
+ return mAdEventType;
+ }
+
+ /**
+ * Gets the ad counter key for the ad counter histogram override.
+ *
+ * <p>The ad counter key is used with the ad event type from {@link #getAdEventType()} and the
+ * buyer adtech from {@link #getBuyer()} to specify which histogram to use in ad selection
+ * filtering. The ad counter key would normally be specified by a custom audience ad to
+ * represent a grouping to filter on.
+ */
+ @NonNull
+ public String getAdCounterKey() {
+ return mAdCounterKey;
+ }
+
+ /**
+ * Gets the {@link AdTechIdentifier} for the buyer which owns the ad counter histogram.
+ *
+ * <p>During filtering in FLEDGE ad selection, ads can only use ad counter histogram data
+ * generated by the same buyer. For {@link FrequencyCapFilters#AD_EVENT_TYPE_WIN}, ad counter
+ * histogram data is further restricted to ads from the same custom audience, which is
+ * identified by the buyer, the custom audience's owner app package name, and the custom
+ * audience name.
+ */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "RemoveAdCounterHistogramOverrideInput{"
+ + "mAdEventType="
+ + mAdEventType
+ + ", mAdCounterKey='"
+ + mAdCounterKey
+ + "', mBuyer="
+ + mBuyer
+ + '}';
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ dest.writeInt(mAdEventType);
+ dest.writeString(mAdCounterKey);
+ dest.writeString(mBuyer.toString());
+ }
+
+ /** Builder for {@link RemoveAdCounterHistogramOverrideInput} objects. */
+ public static final class Builder {
+ @FrequencyCapFilters.AdEventType private int mAdEventType = AD_EVENT_TYPE_INVALID;
+ @Nullable private String mAdCounterKey;
+ @Nullable private AdTechIdentifier mBuyer;
+
+ public Builder() {}
+
+ /**
+ * Sets the {@link FrequencyCapFilters.AdEventType} for the ad counter histogram override.
+ *
+ * <p>See {@link #getAdEventType()} for more information.
+ */
+ @NonNull
+ public Builder setAdEventType(@FrequencyCapFilters.AdEventType int adEventType) {
+ mAdEventType = adEventType;
+ return this;
+ }
+
+ /**
+ * Sets the ad counter key for the ad counter histogram override.
+ *
+ * <p>See {@link #getAdCounterKey()} for more information.
+ */
+ @NonNull
+ public Builder setAdCounterKey(@NonNull String adCounterKey) {
+ Objects.requireNonNull(adCounterKey, NULL_AD_COUNTER_KEY_MESSAGE);
+ mAdCounterKey = adCounterKey;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AdTechIdentifier} for the buyer which owns the ad counter histogram.
+ *
+ * <p>See {@link #getBuyer()} for more information.
+ */
+ @NonNull
+ public Builder setBuyer(@NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer, NULL_BUYER_MESSAGE);
+ mBuyer = buyer;
+ return this;
+ }
+
+ /**
+ * Builds the {@link RemoveAdCounterHistogramOverrideInput} object.
+ *
+ * @throws NullPointerException if any parameters are not set
+ * @throws IllegalArgumentException if the ad event type is invalid
+ */
+ @NonNull
+ public RemoveAdCounterHistogramOverrideInput build()
+ throws NullPointerException, IllegalArgumentException {
+ Preconditions.checkArgument(
+ mAdEventType != AD_EVENT_TYPE_INVALID, UNSET_AD_EVENT_TYPE_MESSAGE);
+ Objects.requireNonNull(mAdCounterKey, NULL_AD_COUNTER_KEY_MESSAGE);
+ Objects.requireNonNull(mBuyer, NULL_BUYER_MESSAGE);
+
+ return new RemoveAdCounterHistogramOverrideInput(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/RemoveAdCounterHistogramOverrideRequest.java b/android-34/android/adservices/adselection/RemoveAdCounterHistogramOverrideRequest.java
new file mode 100644
index 0000000..c068e61
--- /dev/null
+++ b/android-34/android/adservices/adselection/RemoveAdCounterHistogramOverrideRequest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_AD_COUNTER_KEY_MESSAGE;
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_BUYER_MESSAGE;
+import static android.adservices.adselection.UpdateAdCounterHistogramRequest.UNSET_AD_EVENT_TYPE_MESSAGE;
+import static android.adservices.common.FrequencyCapFilters.AD_EVENT_TYPE_INVALID;
+
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.FrequencyCapFilters;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Request object for removing ad counter histogram overrides.
+ *
+ * <p>Histogram overrides replace actual ad counter histograms used in ad selection. Overrides may
+ * only be set in debuggable apps on phones running a debuggable OS build with developer options
+ * enabled. Overrides are only available from the calling app.
+ *
+ * @hide
+ */
+// TODO(b/221876775): Unhide for frequency cap API review
+public class RemoveAdCounterHistogramOverrideRequest {
+ @FrequencyCapFilters.AdEventType private final int mAdEventType;
+ @NonNull private final String mAdCounterKey;
+ @NonNull private final AdTechIdentifier mBuyer;
+
+ private RemoveAdCounterHistogramOverrideRequest(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mAdEventType = builder.mAdEventType;
+ mAdCounterKey = builder.mAdCounterKey;
+ mBuyer = builder.mBuyer;
+ }
+
+ /**
+ * Gets the {@link FrequencyCapFilters.AdEventType} for the ad counter histogram override.
+ *
+ * <p>The ad event type is used with the ad counter key from {@link #getAdCounterKey()} and the
+ * buyer adtech from {@link #getBuyer()} to specify which histogram to use in ad selection
+ * filtering. The ad event type would normally be specified by an app/SDK after a
+ * FLEDGE-selected ad is rendered.
+ */
+ @FrequencyCapFilters.AdEventType
+ public int getAdEventType() {
+ return mAdEventType;
+ }
+
+ /**
+ * Gets the ad counter key for the ad counter histogram override.
+ *
+ * <p>The ad counter key is used with the ad event type from {@link #getAdEventType()} and the
+ * buyer adtech from {@link #getBuyer()} to specify which histogram to use in ad selection
+ * filtering. The ad counter key would normally be specified by a custom audience ad to
+ * represent a grouping to filter on.
+ */
+ @NonNull
+ public String getAdCounterKey() {
+ return mAdCounterKey;
+ }
+
+ /**
+ * Gets the {@link AdTechIdentifier} for the buyer which owns the ad counter histogram.
+ *
+ * <p>During filtering in FLEDGE ad selection, ads can only use ad counter histogram data
+ * generated by the same buyer. For {@link FrequencyCapFilters#AD_EVENT_TYPE_WIN}, ad counter
+ * histogram data is further restricted to ads from the same custom audience, which is
+ * identified by the buyer, the custom audience's owner app package name, and the custom
+ * audience name.
+ */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ @Override
+ public String toString() {
+ return "RemoveAdCounterHistogramOverrideRequest{"
+ + "mAdEventType="
+ + mAdEventType
+ + ", mAdCounterKey='"
+ + mAdCounterKey
+ + "', mBuyer="
+ + mBuyer
+ + '}';
+ }
+
+ /** Builder for {@link RemoveAdCounterHistogramOverrideRequest} objects. */
+ public static final class Builder {
+ @FrequencyCapFilters.AdEventType private int mAdEventType = AD_EVENT_TYPE_INVALID;
+ @Nullable private String mAdCounterKey;
+ @Nullable private AdTechIdentifier mBuyer;
+
+ public Builder() {}
+
+ /**
+ * Sets the {@link FrequencyCapFilters.AdEventType} for the ad counter histogram override.
+ *
+ * <p>See {@link #getAdEventType()} for more information.
+ */
+ @NonNull
+ public Builder setAdEventType(@FrequencyCapFilters.AdEventType int adEventType) {
+ mAdEventType = adEventType;
+ return this;
+ }
+
+ /**
+ * Sets the ad counter key for the ad counter histogram override.
+ *
+ * <p>See {@link #getAdCounterKey()} for more information.
+ */
+ @NonNull
+ public Builder setAdCounterKey(@NonNull String adCounterKey) {
+ Objects.requireNonNull(adCounterKey, NULL_AD_COUNTER_KEY_MESSAGE);
+ mAdCounterKey = adCounterKey;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AdTechIdentifier} for the buyer which owns the ad counter histogram.
+ *
+ * <p>See {@link #getBuyer()} for more information.
+ */
+ @NonNull
+ public Builder setBuyer(@NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer, NULL_BUYER_MESSAGE);
+ mBuyer = buyer;
+ return this;
+ }
+
+ /**
+ * Builds the {@link RemoveAdCounterHistogramOverrideRequest} object.
+ *
+ * @throws NullPointerException if any parameters are not set
+ * @throws IllegalArgumentException if the ad event type is invalid
+ */
+ @NonNull
+ public RemoveAdCounterHistogramOverrideRequest build()
+ throws NullPointerException, IllegalArgumentException {
+ Preconditions.checkArgument(
+ mAdEventType != AD_EVENT_TYPE_INVALID, UNSET_AD_EVENT_TYPE_MESSAGE);
+ Objects.requireNonNull(mAdCounterKey, NULL_AD_COUNTER_KEY_MESSAGE);
+ Objects.requireNonNull(mBuyer, NULL_BUYER_MESSAGE);
+
+ return new RemoveAdCounterHistogramOverrideRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/RemoveAdSelectionFromOutcomesOverrideRequest.java b/android-34/android/adservices/adselection/RemoveAdSelectionFromOutcomesOverrideRequest.java
new file mode 100644
index 0000000..37071a8
--- /dev/null
+++ b/android-34/android/adservices/adselection/RemoveAdSelectionFromOutcomesOverrideRequest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.annotation.NonNull;
+
+/**
+ * This POJO represents the {@link TestAdSelectionManager
+ * #removeAdSelectionFromOutcomesConfigRemoteInfoOverride(
+ * RemoveAdSelectionFromOutcomesOverrideRequest, Executor, OutcomeReceiver)} request
+ *
+ * <p>It contains one field, a {@link AdSelectionFromOutcomesConfig} which serves as the identifier
+ * of the override to be removed
+ *
+ * @hide
+ */
+public class RemoveAdSelectionFromOutcomesOverrideRequest {
+ @NonNull private final AdSelectionFromOutcomesConfig mAdSelectionFromOutcomesConfig;
+
+ /** Builds a {@link RemoveAdSelectionOverrideRequest} instance. */
+ public RemoveAdSelectionFromOutcomesOverrideRequest(
+ @NonNull AdSelectionFromOutcomesConfig config) {
+ mAdSelectionFromOutcomesConfig = config;
+ }
+
+ /** @return AdSelectionConfig, the configuration of the ad selection process. */
+ @NonNull
+ public AdSelectionFromOutcomesConfig getAdSelectionFromOutcomesConfig() {
+ return mAdSelectionFromOutcomesConfig;
+ }
+}
diff --git a/android-34/android/adservices/adselection/RemoveAdSelectionOverrideRequest.java b/android-34/android/adservices/adselection/RemoveAdSelectionOverrideRequest.java
new file mode 100644
index 0000000..79e8792
--- /dev/null
+++ b/android-34/android/adservices/adselection/RemoveAdSelectionOverrideRequest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.annotation.NonNull;
+import android.os.OutcomeReceiver;
+
+import java.util.concurrent.Executor;
+
+/**
+ * This POJO represents the {@link TestAdSelectionManager#removeAdSelectionConfigRemoteInfoOverride(
+ * RemoveAdSelectionOverrideRequest, Executor, OutcomeReceiver)} request
+ *
+ * <p>It contains one field, a {@link AdSelectionConfig} which serves as the identifier of the
+ * override to be removed
+ */
+public class RemoveAdSelectionOverrideRequest {
+ @NonNull private final AdSelectionConfig mAdSelectionConfig;
+
+ /** Builds a {@link RemoveAdSelectionOverrideRequest} instance. */
+ public RemoveAdSelectionOverrideRequest(@NonNull AdSelectionConfig adSelectionConfig) {
+ mAdSelectionConfig = adSelectionConfig;
+ }
+
+ /**
+ * @return AdSelectionConfig, the configuration of the ad selection process.
+ */
+ @NonNull
+ public AdSelectionConfig getAdSelectionConfig() {
+ return mAdSelectionConfig;
+ }
+}
diff --git a/android-34/android/adservices/adselection/ReportImpressionInput.java b/android-34/android/adservices/adselection/ReportImpressionInput.java
new file mode 100644
index 0000000..3e09b17
--- /dev/null
+++ b/android-34/android/adservices/adselection/ReportImpressionInput.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID;
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID_MESSAGE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Represent input params to the reportImpression API.
+ *
+ * @hide
+ */
+public final class ReportImpressionInput implements Parcelable {
+ private final long mAdSelectionId;
+ @NonNull private final AdSelectionConfig mAdSelectionConfig;
+ @NonNull private final String mCallerPackageName;
+
+ @NonNull
+ public static final Parcelable.Creator<ReportImpressionInput> CREATOR =
+ new Parcelable.Creator<ReportImpressionInput>() {
+ public ReportImpressionInput createFromParcel(Parcel in) {
+ return new ReportImpressionInput(in);
+ }
+
+ public ReportImpressionInput[] newArray(int size) {
+ return new ReportImpressionInput[size];
+ }
+ };
+
+ private ReportImpressionInput(
+ long adSelectionId,
+ @NonNull AdSelectionConfig adSelectionConfig,
+ @NonNull String callerPackageName) {
+ Objects.requireNonNull(adSelectionConfig);
+
+ this.mAdSelectionId = adSelectionId;
+ this.mAdSelectionConfig = adSelectionConfig;
+ this.mCallerPackageName = callerPackageName;
+ }
+
+ private ReportImpressionInput(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ this.mAdSelectionId = in.readLong();
+ this.mAdSelectionConfig = AdSelectionConfig.CREATOR.createFromParcel(in);
+ this.mCallerPackageName = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ dest.writeLong(mAdSelectionId);
+ mAdSelectionConfig.writeToParcel(dest, flags);
+ dest.writeString(mCallerPackageName);
+ }
+
+ /**
+ * Returns the adSelectionId, one of the inputs to {@link ReportImpressionInput} as noted in
+ * {@code AdSelectionService}.
+ */
+ public long getAdSelectionId() {
+ return mAdSelectionId;
+ }
+
+ /**
+ * Returns the adSelectionConfig, one of the inputs to {@link ReportImpressionInput} as noted in
+ * {@code AdSelectionService}.
+ */
+ @NonNull
+ public AdSelectionConfig getAdSelectionConfig() {
+ return mAdSelectionConfig;
+ }
+
+ /** @return the caller package name */
+ @NonNull
+ public String getCallerPackageName() {
+ return mCallerPackageName;
+ }
+
+ /**
+ * Builder for {@link ReportImpressionInput} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ private long mAdSelectionId = UNSET_AD_SELECTION_ID;
+ @Nullable private AdSelectionConfig mAdSelectionConfig;
+ private String mCallerPackageName;
+
+ public Builder() {}
+
+ /** Set the mAdSelectionId. */
+ @NonNull
+ public ReportImpressionInput.Builder setAdSelectionId(long adSelectionId) {
+ this.mAdSelectionId = adSelectionId;
+ return this;
+ }
+
+ /** Set the AdSelectionConfig. */
+ @NonNull
+ public ReportImpressionInput.Builder setAdSelectionConfig(
+ @NonNull AdSelectionConfig adSelectionConfig) {
+ Objects.requireNonNull(adSelectionConfig);
+
+ this.mAdSelectionConfig = adSelectionConfig;
+ return this;
+ }
+
+ /** Sets the caller's package name. */
+ @NonNull
+ public ReportImpressionInput.Builder setCallerPackageName(
+ @NonNull String callerPackageName) {
+ Objects.requireNonNull(callerPackageName);
+
+ this.mCallerPackageName = callerPackageName;
+ return this;
+ }
+
+ /** Builds a {@link ReportImpressionInput} instance. */
+ @NonNull
+ public ReportImpressionInput build() {
+ Objects.requireNonNull(mAdSelectionConfig);
+ Objects.requireNonNull(mCallerPackageName);
+
+ Preconditions.checkArgument(
+ mAdSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
+
+ return new ReportImpressionInput(
+ mAdSelectionId, mAdSelectionConfig, mCallerPackageName);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/ReportImpressionRequest.java b/android-34/android/adservices/adselection/ReportImpressionRequest.java
new file mode 100644
index 0000000..333cacf
--- /dev/null
+++ b/android-34/android/adservices/adselection/ReportImpressionRequest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID;
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID_MESSAGE;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Represent input parameters to the reportImpression API.
+ */
+public class ReportImpressionRequest {
+ private final long mAdSelectionId;
+ @NonNull private final AdSelectionConfig mAdSelectionConfig;
+
+ public ReportImpressionRequest(
+ long adSelectionId, @NonNull AdSelectionConfig adSelectionConfig) {
+ Objects.requireNonNull(adSelectionConfig);
+ Preconditions.checkArgument(
+ adSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
+
+ mAdSelectionId = adSelectionId;
+ mAdSelectionConfig = adSelectionConfig;
+ }
+
+ /** Returns the adSelectionId, one of the inputs to {@link ReportImpressionRequest} */
+ public long getAdSelectionId() {
+ return mAdSelectionId;
+ }
+
+ /** Returns the adSelectionConfig, one of the inputs to {@link ReportImpressionRequest} */
+ @NonNull
+ public AdSelectionConfig getAdSelectionConfig() {
+ return mAdSelectionConfig;
+ }
+}
diff --git a/android-34/android/adservices/adselection/ReportInteractionInput.java b/android-34/android/adservices/adselection/ReportInteractionInput.java
new file mode 100644
index 0000000..7385575
--- /dev/null
+++ b/android-34/android/adservices/adselection/ReportInteractionInput.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID;
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID_MESSAGE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Input object wrapping the required arguments needed to report an interaction.
+ *
+ * @hide
+ */
+public class ReportInteractionInput implements Parcelable {
+
+ private static final int UNSET_REPORTING_DESTINATIONS = 0;
+ private static final String UNSET_REPORTING_DESTINATIONS_MESSAGE =
+ "Reporting Destinations bitfield not set.";
+
+ private final long mAdSelectionId;
+ @NonNull private final String mInteractionKey;
+ @NonNull private final String mInteractionData;
+ @NonNull private final String mCallerPackageName;
+ private final int mReportingDestinations; // buyer, seller, or both
+
+ @NonNull
+ public static final Creator<ReportInteractionInput> CREATOR =
+ new Creator<ReportInteractionInput>() {
+ @Override
+ public ReportInteractionInput createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ return new ReportInteractionInput(in);
+ }
+
+ @Override
+ public ReportInteractionInput[] newArray(int size) {
+ return new ReportInteractionInput[size];
+ }
+ };
+
+ private ReportInteractionInput(
+ long adSelectionId,
+ @NonNull String interactionKey,
+ @NonNull String interactionData,
+ @NonNull String callerPackageName,
+ int reportingDestinations) {
+ Objects.requireNonNull(interactionKey);
+ Objects.requireNonNull(interactionData);
+ Objects.requireNonNull(callerPackageName);
+
+ this.mAdSelectionId = adSelectionId;
+ this.mInteractionKey = interactionKey;
+ this.mInteractionData = interactionData;
+ this.mCallerPackageName = callerPackageName;
+ this.mReportingDestinations = reportingDestinations;
+ }
+
+ private ReportInteractionInput(@NonNull Parcel in) {
+ this.mAdSelectionId = in.readLong();
+ this.mInteractionKey = in.readString();
+ this.mInteractionData = in.readString();
+ this.mCallerPackageName = in.readString();
+ this.mReportingDestinations = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ dest.writeLong(mAdSelectionId);
+ dest.writeString(mInteractionKey);
+ dest.writeString(mInteractionData);
+ dest.writeString(mCallerPackageName);
+ dest.writeInt(mReportingDestinations);
+ }
+
+ /** Returns the adSelectionId, the primary identifier of an ad selection process. */
+ public long getAdSelectionId() {
+ return mAdSelectionId;
+ }
+
+ /**
+ * Returns the interaction key, the type of interaction to be reported. This will be used to
+ * fetch the {@code interactionReportingUri} associated with the {@code interactionKey}
+ * registered in {@code registerAdBeacon} after ad selection.
+ */
+ @NonNull
+ public String getInteractionKey() {
+ return mInteractionKey;
+ }
+
+ /**
+ * Returns the interaction data. After ad selection, this data is generated by the caller, and
+ * will be attached in a POST request to the {@code interactionReportingUri} registered in
+ * {@code registerAdBeacon}.
+ */
+ @NonNull
+ public String getInteractionData() {
+ return mInteractionData;
+ }
+
+ /** @return the caller package name */
+ @NonNull
+ public String getCallerPackageName() {
+ return mCallerPackageName;
+ }
+
+ /** Returns the bitfield of reporting destinations to report to (buyer, seller, or both) */
+ public int getReportingDestinations() {
+ return mReportingDestinations;
+ }
+
+ /**
+ * Builder for {@link ReportInteractionInput} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ private long mAdSelectionId = UNSET_AD_SELECTION_ID;
+ @Nullable private String mInteractionKey;
+ @Nullable private String mInteractionData;
+ @Nullable private String mCallerPackageName;
+ private int mReportingDestinations = UNSET_REPORTING_DESTINATIONS;
+
+ public Builder() {}
+
+ /** Sets the adSelectionId. */
+ @NonNull
+ public ReportInteractionInput.Builder setAdSelectionId(long adSelectionId) {
+ mAdSelectionId = adSelectionId;
+ return this;
+ }
+
+ /** Sets the interactionKey. */
+ @NonNull
+ public ReportInteractionInput.Builder setInteractionKey(@NonNull String interactionKey) {
+ Objects.requireNonNull(interactionKey);
+
+ mInteractionKey = interactionKey;
+ return this;
+ }
+
+ /** Sets the interactionData. */
+ @NonNull
+ public ReportInteractionInput.Builder setInteractionData(@NonNull String interactionData) {
+ Objects.requireNonNull(interactionData);
+
+ mInteractionData = interactionData;
+ return this;
+ }
+
+ /** Sets the caller's package name. */
+ @NonNull
+ public ReportInteractionInput.Builder setCallerPackageName(
+ @NonNull String callerPackageName) {
+ Objects.requireNonNull(callerPackageName);
+
+ this.mCallerPackageName = callerPackageName;
+ return this;
+ }
+
+ /** Sets the bitfield of reporting destinations. */
+ @NonNull
+ public ReportInteractionInput.Builder setReportingDestinations(int reportingDestinations) {
+ Preconditions.checkArgument(
+ reportingDestinations != UNSET_REPORTING_DESTINATIONS,
+ UNSET_REPORTING_DESTINATIONS_MESSAGE);
+
+ mReportingDestinations = reportingDestinations;
+ return this;
+ }
+
+ /** Builds a {@link ReportInteractionInput} instance. */
+ @NonNull
+ public ReportInteractionInput build() {
+ Objects.requireNonNull(mInteractionKey);
+ Objects.requireNonNull(mInteractionData);
+ Objects.requireNonNull(mCallerPackageName);
+
+ Preconditions.checkArgument(
+ mAdSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
+ Preconditions.checkArgument(
+ mReportingDestinations != UNSET_REPORTING_DESTINATIONS,
+ UNSET_REPORTING_DESTINATIONS_MESSAGE);
+
+ return new ReportInteractionInput(
+ mAdSelectionId,
+ mInteractionKey,
+ mInteractionData,
+ mCallerPackageName,
+ mReportingDestinations);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/ReportInteractionRequest.java b/android-34/android/adservices/adselection/ReportInteractionRequest.java
new file mode 100644
index 0000000..f18d4a2
--- /dev/null
+++ b/android-34/android/adservices/adselection/ReportInteractionRequest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID;
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID_MESSAGE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Request object wrapping the required arguments needed to report an interaction.
+ *
+ * @hide
+ */
+// TODO(b/261812140): Unhide for report interaction API review
+public class ReportInteractionRequest {
+ public static final int FLAG_REPORTING_DESTINATION_SELLER = 1 << 0;
+ public static final int FLAG_REPORTING_DESTINATION_BUYER = 1 << 1;
+ private static final int UNSET_REPORTING_DESTINATIONS = 0;
+ private static final String UNSET_REPORTING_DESTINATIONS_MESSAGE =
+ "Reporting destinations bitfield not set.";
+
+ private final long mAdSelectionId;
+ @NonNull private final String mInteractionKey;
+ @NonNull private final String mInteractionData;
+ @ReportingDestination private final int mReportingDestinations; // buyer, seller, or both
+
+ public ReportInteractionRequest(
+ long adSelectionId,
+ @NonNull String interactionKey,
+ @NonNull String interactionData,
+ @ReportingDestination int reportingDestinations) {
+ Objects.requireNonNull(interactionKey);
+ Objects.requireNonNull(interactionData);
+
+ Preconditions.checkArgument(
+ adSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
+ Preconditions.checkArgument(
+ reportingDestinations != UNSET_REPORTING_DESTINATIONS,
+ UNSET_REPORTING_DESTINATIONS_MESSAGE);
+
+ this.mAdSelectionId = adSelectionId;
+ this.mInteractionKey = interactionKey;
+ this.mInteractionData = interactionData;
+ this.mReportingDestinations = reportingDestinations;
+ }
+
+ /** Returns the adSelectionId, the primary identifier of an ad selection process. */
+ public long getAdSelectionId() {
+ return mAdSelectionId;
+ }
+
+ /**
+ * Returns the interaction key, the type of interaction to be reported.
+ *
+ * <p>This will be used to fetch the {@code interactionReportingUri} associated with the {@code
+ * interactionKey} registered in {@code registerAdBeacon} after ad selection.
+ */
+ @NonNull
+ public String getInteractionKey() {
+ return mInteractionKey;
+ }
+
+ /**
+ * Returns the interaction data.
+ *
+ * <p>After ad selection, this data is generated by the caller, and will be attached in a POST
+ * request to the {@code interactionReportingUri} registered in {@code registerAdBeacon}.
+ */
+ @NonNull
+ public String getInteractionData() {
+ return mInteractionData;
+ }
+
+ /**
+ * Returns the bitfield of reporting destinations to report to (buyer, seller, or both).
+ *
+ * <p>To create this bitfield, place an {@code |} bitwise operator between each {@code
+ * reportingDestination} to be reported to. For example to only report to buyer, set the
+ * reportingDestinations field to {@link #FLAG_REPORTING_DESTINATION_BUYER} To only report to
+ * seller, set the reportingDestinations field to {@link #FLAG_REPORTING_DESTINATION_SELLER} To
+ * report to both buyers and sellers, set the reportingDestinations field to {@link
+ * #FLAG_REPORTING_DESTINATION_BUYER} | {@link #FLAG_REPORTING_DESTINATION_SELLER}
+ */
+ @ReportingDestination
+ public int getReportingDestinations() {
+ return mReportingDestinations;
+ }
+
+ /** @hide */
+ @IntDef(
+ flag = true,
+ prefix = {"FLAG_REPORTING_DESTINATION"},
+ value = {FLAG_REPORTING_DESTINATION_SELLER, FLAG_REPORTING_DESTINATION_BUYER})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ReportingDestination {}
+}
diff --git a/android-34/android/adservices/adselection/SetAdCounterHistogramOverrideInput.java b/android-34/android/adservices/adselection/SetAdCounterHistogramOverrideInput.java
new file mode 100644
index 0000000..6517fb1
--- /dev/null
+++ b/android-34/android/adservices/adselection/SetAdCounterHistogramOverrideInput.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_AD_COUNTER_KEY_MESSAGE;
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_BUYER_MESSAGE;
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_CUSTOM_AUDIENCE_NAME_MESSAGE;
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_CUSTOM_AUDIENCE_OWNER_MESSAGE;
+import static android.adservices.adselection.SetAdCounterHistogramOverrideRequest.NULL_HISTOGRAM_TIMESTAMPS_MESSAGE;
+import static android.adservices.adselection.UpdateAdCounterHistogramRequest.UNSET_AD_EVENT_TYPE_MESSAGE;
+import static android.adservices.common.FrequencyCapFilters.AD_EVENT_TYPE_INVALID;
+
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.FrequencyCapFilters;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+import com.android.internal.util.Preconditions;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Input object for setting ad counter histogram overrides.
+ *
+ * <p>Histogram overrides replace actual ad counter histograms used in ad selection. Overrides may
+ * only be set in debuggable apps on phones running a debuggable OS build with developer options
+ * enabled. Overrides are only available from the calling app.
+ *
+ * @hide
+ */
+public final class SetAdCounterHistogramOverrideInput implements Parcelable {
+ @FrequencyCapFilters.AdEventType private final int mAdEventType;
+ @NonNull private final String mAdCounterKey;
+ @NonNull private final List<Instant> mHistogramTimestamps;
+ @NonNull private final AdTechIdentifier mBuyer;
+ @NonNull private final String mCustomAudienceOwner;
+ @NonNull private final String mCustomAudienceName;
+
+ @NonNull
+ public static final Creator<SetAdCounterHistogramOverrideInput> CREATOR =
+ new Creator<SetAdCounterHistogramOverrideInput>() {
+ @Override
+ public SetAdCounterHistogramOverrideInput createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ return new SetAdCounterHistogramOverrideInput(in);
+ }
+
+ @Override
+ public SetAdCounterHistogramOverrideInput[] newArray(int size) {
+ return new SetAdCounterHistogramOverrideInput[size];
+ }
+ };
+
+ private SetAdCounterHistogramOverrideInput(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mAdEventType = builder.mAdEventType;
+ mAdCounterKey = builder.mAdCounterKey;
+ mHistogramTimestamps = builder.mHistogramTimestamps;
+ mBuyer = builder.mBuyer;
+ mCustomAudienceOwner = builder.mCustomAudienceOwner;
+ mCustomAudienceName = builder.mCustomAudienceName;
+ }
+
+ private SetAdCounterHistogramOverrideInput(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mAdEventType = in.readInt();
+ mAdCounterKey = in.readString();
+ mHistogramTimestamps = AdServicesParcelableUtil.readInstantListFromParcel(in);
+ mBuyer = AdTechIdentifier.fromString(in.readString());
+ mCustomAudienceOwner = in.readString();
+ mCustomAudienceName = in.readString();
+ }
+
+ /**
+ * Gets the {@link FrequencyCapFilters.AdEventType} for the ad counter histogram override.
+ *
+ * <p>The ad event type is used with the ad counter key from {@link #getAdCounterKey()} and the
+ * buyer adtech from {@link #getBuyer()} to specify which histogram to use in ad selection
+ * filtering. The ad event type would normally be specified by an app/SDK after a
+ * FLEDGE-selected ad is rendered.
+ */
+ @FrequencyCapFilters.AdEventType
+ public int getAdEventType() {
+ return mAdEventType;
+ }
+
+ /**
+ * Gets the ad counter key for the ad counter histogram override.
+ *
+ * <p>The ad counter key is used with the ad event type from {@link #getAdEventType()} and the
+ * buyer adtech from {@link #getBuyer()} to specify which histogram to use in ad selection
+ * filtering. The ad counter key would normally be specified by a custom audience ad to
+ * represent a grouping to filter on.
+ */
+ @NonNull
+ public String getAdCounterKey() {
+ return mAdCounterKey;
+ }
+
+ /**
+ * Gets the list of {@link Instant} objects for the ad counter histogram override.
+ *
+ * <p>When set, this list of timestamps is used to populate the override histogram, which is
+ * used instead of actual histograms for ad selection filtering.
+ */
+ @NonNull
+ public List<Instant> getHistogramTimestamps() {
+ return mHistogramTimestamps;
+ }
+
+ /**
+ * Gets the {@link AdTechIdentifier} for the buyer which owns the ad counter histogram.
+ *
+ * <p>During filtering in FLEDGE ad selection, ads can only use ad counter histogram data
+ * generated by the same buyer. For {@link FrequencyCapFilters#AD_EVENT_TYPE_WIN}, ad counter
+ * histogram data is further restricted to ads from the same custom audience, which is
+ * identified by the buyer, the custom audience's owner app package name from {@link
+ * #getCustomAudienceOwner()}, and the custom audience name from {@link
+ * #getCustomAudienceName()}.
+ */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ /**
+ * Gets the package name for the app which generated the custom audience which is associated
+ * with the overridden ad counter histogram data.
+ *
+ * <p>For {@link FrequencyCapFilters#AD_EVENT_TYPE_WIN}, ad counter histogram data is restricted
+ * to ads from the same custom audience, which is identified by the buyer from {@link
+ * #getBuyer()}, the custom audience's owner app package name, and the custom audience name from
+ * {@link #getCustomAudienceName()}.
+ */
+ @NonNull
+ public String getCustomAudienceOwner() {
+ return mCustomAudienceOwner;
+ }
+
+ /**
+ * Gets the buyer-generated name for the custom audience which is associated with the overridden
+ * ad counter histogram data.
+ *
+ * <p>For {@link FrequencyCapFilters#AD_EVENT_TYPE_WIN}, ad counter histogram data is restricted
+ * to ads from the same custom audience, which is identified by the buyer from {@link
+ * #getBuyer()}, the custom audience's owner app package name from {@link
+ * #getCustomAudienceOwner()}, and the custom audience name.
+ */
+ @NonNull
+ public String getCustomAudienceName() {
+ return mCustomAudienceName;
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "SetAdCounterHistogramOverrideInput{"
+ + "mAdEventType="
+ + mAdEventType
+ + ", mAdCounterKey='"
+ + mAdCounterKey
+ + "', mHistogramTimestamps="
+ + mHistogramTimestamps
+ + ", mBuyer="
+ + mBuyer
+ + ", mCustomAudienceOwner='"
+ + mCustomAudienceOwner
+ + "', mCustomAudienceName='"
+ + mCustomAudienceName
+ + "'}";
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ dest.writeInt(mAdEventType);
+ dest.writeString(mAdCounterKey);
+ AdServicesParcelableUtil.writeInstantListToParcel(dest, mHistogramTimestamps);
+ dest.writeString(mBuyer.toString());
+ dest.writeString(mCustomAudienceOwner);
+ dest.writeString(mCustomAudienceName);
+ }
+
+ /** Builder for {@link SetAdCounterHistogramOverrideInput} objects. */
+ public static final class Builder {
+ @FrequencyCapFilters.AdEventType private int mAdEventType = AD_EVENT_TYPE_INVALID;
+ @Nullable private String mAdCounterKey;
+ @NonNull private List<Instant> mHistogramTimestamps = new ArrayList<>();
+ @Nullable private AdTechIdentifier mBuyer;
+ @Nullable private String mCustomAudienceOwner;
+ @Nullable private String mCustomAudienceName;
+
+ public Builder() {}
+
+ /**
+ * Sets the {@link FrequencyCapFilters.AdEventType} for the ad counter histogram override.
+ *
+ * <p>See {@link #getAdEventType()} for more information.
+ */
+ @NonNull
+ public Builder setAdEventType(@FrequencyCapFilters.AdEventType int adEventType) {
+ mAdEventType = adEventType;
+ return this;
+ }
+
+ /**
+ * Sets the ad counter key for the ad counter histogram override.
+ *
+ * <p>See {@link #getAdCounterKey()} for more information.
+ */
+ @NonNull
+ public Builder setAdCounterKey(@NonNull String adCounterKey) {
+ Objects.requireNonNull(adCounterKey, NULL_AD_COUNTER_KEY_MESSAGE);
+ mAdCounterKey = adCounterKey;
+ return this;
+ }
+
+ /**
+ * Sets the list of {@link Instant} objects for the ad counter histogram override.
+ *
+ * <p>See {@link #getHistogramTimestamps()} for more information.
+ */
+ @NonNull
+ public Builder setHistogramTimestamps(@NonNull List<Instant> histogramTimestamps) {
+ Objects.requireNonNull(histogramTimestamps, NULL_HISTOGRAM_TIMESTAMPS_MESSAGE);
+ mHistogramTimestamps = histogramTimestamps;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AdTechIdentifier} for the buyer which owns the ad counter histogram.
+ *
+ * <p>See {@link #getBuyer()} for more information.
+ */
+ @NonNull
+ public Builder setBuyer(@NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer, NULL_BUYER_MESSAGE);
+ mBuyer = buyer;
+ return this;
+ }
+
+ /**
+ * Sets the package name for the app which generated the custom audience which is associated
+ * with the overridden ad counter histogram data.
+ *
+ * <p>See {@link #getCustomAudienceOwner()} for more information.
+ */
+ @NonNull
+ public Builder setCustomAudienceOwner(@NonNull String customAudienceOwner) {
+ Objects.requireNonNull(customAudienceOwner, NULL_CUSTOM_AUDIENCE_OWNER_MESSAGE);
+ mCustomAudienceOwner = customAudienceOwner;
+ return this;
+ }
+
+ /**
+ * Sets the buyer-generated name for the custom audience which is associated with the
+ * overridden ad counter histogram data.
+ *
+ * <p>See {@link #getCustomAudienceName()} for more information.
+ */
+ @NonNull
+ public Builder setCustomAudienceName(@NonNull String customAudienceName) {
+ Objects.requireNonNull(customAudienceName, NULL_CUSTOM_AUDIENCE_NAME_MESSAGE);
+ mCustomAudienceName = customAudienceName;
+ return this;
+ }
+
+ /**
+ * Builds the {@link SetAdCounterHistogramOverrideInput} object.
+ *
+ * @throws NullPointerException if any parameters are not set
+ * @throws IllegalArgumentException if the ad event type is invalid
+ */
+ @NonNull
+ public SetAdCounterHistogramOverrideInput build()
+ throws NullPointerException, IllegalArgumentException {
+ Preconditions.checkArgument(
+ mAdEventType != AD_EVENT_TYPE_INVALID, UNSET_AD_EVENT_TYPE_MESSAGE);
+ Objects.requireNonNull(mAdCounterKey, NULL_AD_COUNTER_KEY_MESSAGE);
+ Objects.requireNonNull(mBuyer, NULL_BUYER_MESSAGE);
+ Objects.requireNonNull(mCustomAudienceOwner, NULL_CUSTOM_AUDIENCE_OWNER_MESSAGE);
+ Objects.requireNonNull(mCustomAudienceName, NULL_CUSTOM_AUDIENCE_NAME_MESSAGE);
+
+ return new SetAdCounterHistogramOverrideInput(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/SetAdCounterHistogramOverrideRequest.java b/android-34/android/adservices/adselection/SetAdCounterHistogramOverrideRequest.java
new file mode 100644
index 0000000..f39827d
--- /dev/null
+++ b/android-34/android/adservices/adselection/SetAdCounterHistogramOverrideRequest.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.UpdateAdCounterHistogramRequest.UNSET_AD_EVENT_TYPE_MESSAGE;
+import static android.adservices.common.FrequencyCapFilters.AD_EVENT_TYPE_INVALID;
+
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.FrequencyCapFilters;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Request object for setting ad counter histogram overrides.
+ *
+ * <p>Histogram overrides replace actual ad counter histograms used in ad selection. Overrides may
+ * only be set in debuggable apps on phones running a debuggable OS build with developer options
+ * enabled. Overrides are only available from the calling app.
+ *
+ * @hide
+ */
+// TODO(b/221876775): Unhide for frequency cap API review
+public class SetAdCounterHistogramOverrideRequest {
+ /** @hide */
+ public static final String NULL_AD_COUNTER_KEY_MESSAGE = "Ad counter key must not be null";
+
+ /** @hide */
+ public static final String NULL_HISTOGRAM_TIMESTAMPS_MESSAGE =
+ "List of histogram timestamps must not be null";
+
+ /** @hide */
+ public static final String NULL_BUYER_MESSAGE = "Buyer must not be null";
+
+ /** @hide */
+ public static final String NULL_CUSTOM_AUDIENCE_OWNER_MESSAGE =
+ "Custom audience owner must not be null";
+
+ /** @hide */
+ public static final String NULL_CUSTOM_AUDIENCE_NAME_MESSAGE =
+ "Custom audience name must not be null";
+
+ @FrequencyCapFilters.AdEventType private final int mAdEventType;
+ @NonNull private final String mAdCounterKey;
+ @NonNull private final List<Instant> mHistogramTimestamps;
+ @NonNull private final AdTechIdentifier mBuyer;
+ @NonNull private final String mCustomAudienceOwner;
+ @NonNull private final String mCustomAudienceName;
+
+ private SetAdCounterHistogramOverrideRequest(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mAdEventType = builder.mAdEventType;
+ mAdCounterKey = builder.mAdCounterKey;
+ mHistogramTimestamps = builder.mHistogramTimestamps;
+ mBuyer = builder.mBuyer;
+ mCustomAudienceOwner = builder.mCustomAudienceOwner;
+ mCustomAudienceName = builder.mCustomAudienceName;
+ }
+
+ /**
+ * Gets the {@link FrequencyCapFilters.AdEventType} for the ad counter histogram override.
+ *
+ * <p>The ad event type is used with the ad counter key from {@link #getAdCounterKey()} and the
+ * buyer adtech from {@link #getBuyer()} to specify which histogram to use in ad selection
+ * filtering. The ad event type would normally be specified by an app/SDK after a
+ * FLEDGE-selected ad is rendered.
+ */
+ @FrequencyCapFilters.AdEventType
+ public int getAdEventType() {
+ return mAdEventType;
+ }
+
+ /**
+ * Gets the ad counter key for the ad counter histogram override.
+ *
+ * <p>The ad counter key is used with the ad event type from {@link #getAdEventType()} and the
+ * buyer adtech from {@link #getBuyer()} to specify which histogram to use in ad selection
+ * filtering. The ad counter key would normally be specified by a custom audience ad to
+ * represent a grouping to filter on.
+ */
+ @NonNull
+ public String getAdCounterKey() {
+ return mAdCounterKey;
+ }
+
+ /**
+ * Gets the list of {@link Instant} objects for the ad counter histogram override.
+ *
+ * <p>When set, this list of timestamps is used to populate the override histogram, which is
+ * used instead of actual histograms for ad selection filtering.
+ */
+ @NonNull
+ public List<Instant> getHistogramTimestamps() {
+ return mHistogramTimestamps;
+ }
+
+ /**
+ * Gets the {@link AdTechIdentifier} for the buyer which owns the ad counter histogram.
+ *
+ * <p>During filtering in FLEDGE ad selection, ads can only use ad counter histogram data
+ * generated by the same buyer. For {@link FrequencyCapFilters#AD_EVENT_TYPE_WIN}, ad counter
+ * histogram data is further restricted to ads from the same custom audience, which is
+ * identified by the buyer, the custom audience's owner app package name from {@link
+ * #getCustomAudienceOwner()}, and the custom audience name from {@link
+ * #getCustomAudienceName()}.
+ */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ /**
+ * Gets the package name for the app which generated the custom audience which is associated
+ * with the overridden ad counter histogram data.
+ *
+ * <p>For {@link FrequencyCapFilters#AD_EVENT_TYPE_WIN}, ad counter histogram data is restricted
+ * to ads from the same custom audience, which is identified by the buyer from {@link
+ * #getBuyer()}, the custom audience's owner app package name, and the custom audience name from
+ * {@link #getCustomAudienceName()}.
+ */
+ @NonNull
+ public String getCustomAudienceOwner() {
+ return mCustomAudienceOwner;
+ }
+
+ /**
+ * Gets the buyer-generated name for the custom audience which is associated with the overridden
+ * ad counter histogram data.
+ *
+ * <p>For {@link FrequencyCapFilters#AD_EVENT_TYPE_WIN}, ad counter histogram data is restricted
+ * to ads from the same custom audience, which is identified by the buyer from {@link
+ * #getBuyer()}, the custom audience's owner app package name from {@link
+ * #getCustomAudienceOwner()}, and the custom audience name.
+ */
+ @NonNull
+ public String getCustomAudienceName() {
+ return mCustomAudienceName;
+ }
+
+ @Override
+ public String toString() {
+ return "SetAdCounterHistogramOverrideRequest{"
+ + "mAdEventType="
+ + mAdEventType
+ + ", mAdCounterKey='"
+ + mAdCounterKey
+ + "', mHistogramTimestamps="
+ + mHistogramTimestamps
+ + ", mBuyer="
+ + mBuyer
+ + ", mCustomAudienceOwner='"
+ + mCustomAudienceOwner
+ + "', mCustomAudienceName='"
+ + mCustomAudienceName
+ + "'}";
+ }
+
+ /** Builder for {@link SetAdCounterHistogramOverrideRequest} objects. */
+ public static final class Builder {
+ @FrequencyCapFilters.AdEventType private int mAdEventType = AD_EVENT_TYPE_INVALID;
+ @Nullable private String mAdCounterKey;
+ @NonNull private List<Instant> mHistogramTimestamps = new ArrayList<>();
+ @Nullable private AdTechIdentifier mBuyer;
+ @Nullable private String mCustomAudienceOwner;
+ @Nullable private String mCustomAudienceName;
+
+ public Builder() {}
+
+ /**
+ * Sets the {@link FrequencyCapFilters.AdEventType} for the ad counter histogram override.
+ *
+ * <p>See {@link #getAdEventType()} for more information.
+ */
+ @NonNull
+ public Builder setAdEventType(@FrequencyCapFilters.AdEventType int adEventType) {
+ mAdEventType = adEventType;
+ return this;
+ }
+
+ /**
+ * Sets the ad counter key for the ad counter histogram override.
+ *
+ * <p>See {@link #getAdCounterKey()} for more information.
+ */
+ @NonNull
+ public Builder setAdCounterKey(@NonNull String adCounterKey) {
+ Objects.requireNonNull(adCounterKey, NULL_AD_COUNTER_KEY_MESSAGE);
+ mAdCounterKey = adCounterKey;
+ return this;
+ }
+
+ /**
+ * Sets the list of {@link Instant} objects for the ad counter histogram override.
+ *
+ * <p>See {@link #getHistogramTimestamps()} for more information.
+ */
+ @NonNull
+ public Builder setHistogramTimestamps(@NonNull List<Instant> histogramTimestamps) {
+ Objects.requireNonNull(histogramTimestamps, NULL_HISTOGRAM_TIMESTAMPS_MESSAGE);
+ mHistogramTimestamps = histogramTimestamps;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AdTechIdentifier} for the buyer which owns the ad counter histogram.
+ *
+ * <p>See {@link #getBuyer()} for more information.
+ */
+ @NonNull
+ public Builder setBuyer(@NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer, NULL_BUYER_MESSAGE);
+ mBuyer = buyer;
+ return this;
+ }
+
+ /**
+ * Sets the package name for the app which generated the custom audience which is associated
+ * with the overridden ad counter histogram data.
+ *
+ * <p>See {@link #getCustomAudienceOwner()} for more information.
+ */
+ @NonNull
+ public Builder setCustomAudienceOwner(@NonNull String customAudienceOwner) {
+ Objects.requireNonNull(customAudienceOwner, NULL_CUSTOM_AUDIENCE_OWNER_MESSAGE);
+ mCustomAudienceOwner = customAudienceOwner;
+ return this;
+ }
+
+ /**
+ * Sets the buyer-generated name for the custom audience which is associated with the
+ * overridden ad counter histogram data.
+ *
+ * <p>See {@link #getCustomAudienceName()} for more information.
+ */
+ @NonNull
+ public Builder setCustomAudienceName(@NonNull String customAudienceName) {
+ Objects.requireNonNull(customAudienceName, NULL_CUSTOM_AUDIENCE_NAME_MESSAGE);
+ mCustomAudienceName = customAudienceName;
+ return this;
+ }
+
+ /**
+ * Builds the {@link SetAdCounterHistogramOverrideRequest} object.
+ *
+ * @throws NullPointerException if any parameters are not set
+ * @throws IllegalArgumentException if the ad event type is invalid
+ */
+ @NonNull
+ public SetAdCounterHistogramOverrideRequest build()
+ throws NullPointerException, IllegalArgumentException {
+ Preconditions.checkArgument(
+ mAdEventType != AD_EVENT_TYPE_INVALID, UNSET_AD_EVENT_TYPE_MESSAGE);
+ Objects.requireNonNull(mAdCounterKey, NULL_AD_COUNTER_KEY_MESSAGE);
+ Objects.requireNonNull(mBuyer, NULL_BUYER_MESSAGE);
+ Objects.requireNonNull(mCustomAudienceOwner, NULL_CUSTOM_AUDIENCE_OWNER_MESSAGE);
+ Objects.requireNonNull(mCustomAudienceName, NULL_CUSTOM_AUDIENCE_NAME_MESSAGE);
+
+ return new SetAdCounterHistogramOverrideRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/SetAppInstallAdvertisersInput.java b/android-34/android/adservices/adselection/SetAppInstallAdvertisersInput.java
new file mode 100644
index 0000000..938c96e
--- /dev/null
+++ b/android-34/android/adservices/adselection/SetAppInstallAdvertisersInput.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represent input params to the setAppInstallAdvertisers API.
+ *
+ * @hide
+ */
+public final class SetAppInstallAdvertisersInput implements Parcelable {
+ @NonNull private final Set<AdTechIdentifier> mAdvertisers;
+ @NonNull private final String mCallerPackageName;
+
+ @NonNull
+ public static final Creator<SetAppInstallAdvertisersInput> CREATOR =
+ new Creator<SetAppInstallAdvertisersInput>() {
+ @NonNull
+ @Override
+ public SetAppInstallAdvertisersInput createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new SetAppInstallAdvertisersInput(in);
+ }
+
+ @NonNull
+ @Override
+ public SetAppInstallAdvertisersInput[] newArray(int size) {
+ return new SetAppInstallAdvertisersInput[size];
+ }
+ };
+
+ private SetAppInstallAdvertisersInput(
+ @NonNull Set<AdTechIdentifier> advertisers, @NonNull String callerPackageName) {
+ Objects.requireNonNull(advertisers);
+ Objects.requireNonNull(callerPackageName);
+
+ this.mAdvertisers = advertisers;
+ this.mCallerPackageName = callerPackageName;
+ }
+
+ private SetAppInstallAdvertisersInput(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ this.mAdvertisers =
+ AdServicesParcelableUtil.readSetFromParcel(in, AdTechIdentifier.CREATOR);
+ this.mCallerPackageName = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ AdServicesParcelableUtil.writeSetToParcel(dest, mAdvertisers);
+ dest.writeString(mCallerPackageName);
+ }
+
+ /**
+ * Returns the advertisers, one of the inputs to {@link SetAppInstallAdvertisersInput} as noted
+ * in {@code AdSelectionService}.
+ */
+ @NonNull
+ public Set<AdTechIdentifier> getAdvertisers() {
+ return mAdvertisers;
+ }
+
+ /** @return the caller package name */
+ @NonNull
+ public String getCallerPackageName() {
+ return mCallerPackageName;
+ }
+
+ /**
+ * Builder for {@link SetAppInstallAdvertisersInput} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @Nullable private Set<AdTechIdentifier> mAdvertisers;
+ @Nullable private String mCallerPackageName;
+
+ public Builder() {}
+
+ /** Set the advertisers. */
+ @NonNull
+ public SetAppInstallAdvertisersInput.Builder setAdvertisers(
+ @NonNull Set<AdTechIdentifier> advertisers) {
+ Objects.requireNonNull(advertisers);
+ this.mAdvertisers = advertisers;
+ return this;
+ }
+
+ /** Sets the caller's package name. */
+ @NonNull
+ public SetAppInstallAdvertisersInput.Builder setCallerPackageName(
+ @NonNull String callerPackageName) {
+ Objects.requireNonNull(callerPackageName);
+
+ this.mCallerPackageName = callerPackageName;
+ return this;
+ }
+
+ /** Builds a {@link SetAppInstallAdvertisersInput} instance. */
+ @NonNull
+ public SetAppInstallAdvertisersInput build() {
+ Objects.requireNonNull(mAdvertisers);
+ Objects.requireNonNull(mCallerPackageName);
+
+ return new SetAppInstallAdvertisersInput(mAdvertisers, mCallerPackageName);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/SetAppInstallAdvertisersRequest.java b/android-34/android/adservices/adselection/SetAppInstallAdvertisersRequest.java
new file mode 100644
index 0000000..5af0b8e
--- /dev/null
+++ b/android-34/android/adservices/adselection/SetAppInstallAdvertisersRequest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents input parameters to the setAppInstallAdvertiser API.
+ *
+ * @hide
+ */
+public class SetAppInstallAdvertisersRequest {
+ @NonNull private final Set<AdTechIdentifier> mAdvertisers;
+
+ public SetAppInstallAdvertisersRequest(@NonNull Set<AdTechIdentifier> advertisers) {
+ Objects.requireNonNull(advertisers);
+
+ mAdvertisers = advertisers;
+ }
+
+ /**
+ * Returns the set of advertisers that will be able to run app install filters based on this
+ * app's presence on the device after a call to SetAppInstallAdvertisers is made with this as
+ * input.
+ */
+ @NonNull
+ public Set<AdTechIdentifier> getAdvertisers() {
+ return mAdvertisers;
+ }
+}
diff --git a/android-34/android/adservices/adselection/TestAdSelectionManager.java b/android-34/android/adservices/adselection/TestAdSelectionManager.java
new file mode 100644
index 0000000..a8afd87
--- /dev/null
+++ b/android-34/android/adservices/adselection/TestAdSelectionManager.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE;
+
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.FledgeErrorResponse;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.os.Build;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.LoggerFactory;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * {@link TestAdSelectionManager} provides APIs for apps and ad SDKs to test ad selection processes.
+ *
+ * <p>These APIs are intended to be used for end-to-end testing. They are enabled only for
+ * debuggable apps on phones running a debuggable OS build with developer options enabled.
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public class TestAdSelectionManager {
+ private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
+
+ private final AdSelectionManager mAdSelectionManager;
+
+ TestAdSelectionManager(@NonNull AdSelectionManager adSelectionManager) {
+ Objects.requireNonNull(adSelectionManager);
+
+ mAdSelectionManager = adSelectionManager;
+ }
+
+ /**
+ * Overrides the AdSelection API for a given {@link AdSelectionConfig} to avoid fetching data
+ * from remote servers and use the data provided in {@link AddAdSelectionOverrideRequest}
+ * instead. The {@link AddAdSelectionOverrideRequest} is provided by the Ads SDK.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The receiver either returns a {@code void} for a successful run, or an {@link
+ * Exception} indicates the error.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void overrideAdSelectionConfigRemoteInfo(
+ @NonNull AddAdSelectionOverrideRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = mAdSelectionManager.getService();
+ service.overrideAdSelectionConfigRemoteInfo(
+ request.getAdSelectionConfig(),
+ request.getDecisionLogicJs(),
+ request.getTrustedScoringSignals(),
+ request.getBuyersDecisionLogic(),
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Removes an override for {@link AdSelectionConfig} in the Ad Selection API with associated the
+ * data in {@link RemoveAdSelectionOverrideRequest}. The {@link
+ * RemoveAdSelectionOverrideRequest} is provided by the Ads SDK.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The receiver either returns a {@code void} for a successful run, or an {@link
+ * Exception} indicates the error.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void removeAdSelectionConfigRemoteInfoOverride(
+ @NonNull RemoveAdSelectionOverrideRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = mAdSelectionManager.getService();
+ service.removeAdSelectionConfigRemoteInfoOverride(
+ request.getAdSelectionConfig(),
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Removes all override data for {@link AdSelectionConfig} in the Ad Selection API.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The receiver either returns a {@code void} for a successful run, or an {@link
+ * Exception} indicates the error.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void resetAllAdSelectionConfigRemoteOverrides(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = mAdSelectionManager.getService();
+ service.resetAllAdSelectionConfigRemoteOverrides(
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Overrides the AdSelection API for {@link AdSelectionFromOutcomesConfig} to avoid fetching
+ * data from remote servers and use the data provided in {@link
+ * AddAdSelectionFromOutcomesOverrideRequest} instead. The {@link
+ * AddAdSelectionFromOutcomesOverrideRequest} is provided by the Ads SDK.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The receiver either returns a {@code void} for a successful run, or an {@link
+ * Exception} indicates the error.
+ * @hide
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void overrideAdSelectionFromOutcomesConfigRemoteInfo(
+ @NonNull AddAdSelectionFromOutcomesOverrideRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = mAdSelectionManager.getService();
+ service.overrideAdSelectionFromOutcomesConfigRemoteInfo(
+ request.getAdSelectionFromOutcomesConfig(),
+ request.getOutcomeSelectionLogicJs(),
+ request.getOutcomeSelectionTrustedSignals(),
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Removes an override for {@link AdSelectionFromOutcomesConfig} in th Ad Selection API with
+ * associated the data in {@link RemoveAdSelectionOverrideRequest}. The {@link
+ * RemoveAdSelectionOverrideRequest} is provided by the Ads SDK.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The receiver either returns a {@code void} for a successful run, or an {@link
+ * Exception} indicates the error.
+ * @hide
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void removeAdSelectionFromOutcomesConfigRemoteInfoOverride(
+ @NonNull RemoveAdSelectionFromOutcomesOverrideRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = mAdSelectionManager.getService();
+ service.removeAdSelectionFromOutcomesConfigRemoteInfoOverride(
+ request.getAdSelectionFromOutcomesConfig(),
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Removes all override data for {@link AdSelectionFromOutcomesConfig} in the Ad Selection API.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The receiver either returns a {@code void} for a successful run, or an {@link
+ * Exception} indicates the error.
+ * @hide
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void resetAllAdSelectionFromOutcomesConfigRemoteOverrides(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ try {
+ final AdSelectionService service = mAdSelectionManager.getService();
+ service.resetAllAdSelectionFromOutcomesConfigRemoteOverrides(
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service.");
+ receiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service.", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Failure of AdSelection service.", e));
+ }
+ }
+
+ /**
+ * Sets the override for event histogram data, which is used in frequency cap filtering during
+ * ad selection.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * <p>The given {@code outcomeReceiver} either returns an empty {@link Object} if successful or
+ * an {@link Exception} which indicates the error.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * @hide
+ */
+ // TODO(b/221876775): Unhide for frequency cap API review
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void setAdCounterHistogramOverride(
+ @NonNull SetAdCounterHistogramOverrideRequest setRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> outcomeReceiver) {
+ Objects.requireNonNull(setRequest, "Request must not be null");
+ Objects.requireNonNull(executor, "Executor must not be null");
+ Objects.requireNonNull(outcomeReceiver, "Outcome receiver must not be null");
+
+ try {
+ final AdSelectionService service =
+ Objects.requireNonNull(mAdSelectionManager.getService());
+ service.setAdCounterHistogramOverride(
+ new SetAdCounterHistogramOverrideInput.Builder()
+ .setAdEventType(setRequest.getAdEventType())
+ .setAdCounterKey(setRequest.getAdCounterKey())
+ .setHistogramTimestamps(setRequest.getHistogramTimestamps())
+ .setBuyer(setRequest.getBuyer())
+ .setCustomAudienceOwner(setRequest.getCustomAudienceOwner())
+ .setCustomAudienceName(setRequest.getCustomAudienceName())
+ .build(),
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> outcomeReceiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ outcomeReceiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service");
+ outcomeReceiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Remote exception encountered while updating ad counter histogram");
+ outcomeReceiver.onError(new IllegalStateException("Failure of AdSelection service", e));
+ }
+ }
+
+ /**
+ * Removes an override for event histogram data, which is used in frequency cap filtering during
+ * ad selection.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * <p>The given {@code outcomeReceiver} either returns an empty {@link Object} if successful or
+ * an {@link Exception} which indicates the error.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * @hide
+ */
+ // TODO(b/221876775): Unhide for frequency cap API review
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void removeAdCounterHistogramOverride(
+ @NonNull RemoveAdCounterHistogramOverrideRequest removeRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> outcomeReceiver) {
+ Objects.requireNonNull(removeRequest, "Request must not be null");
+ Objects.requireNonNull(executor, "Executor must not be null");
+ Objects.requireNonNull(outcomeReceiver, "Outcome receiver must not be null");
+
+ try {
+ final AdSelectionService service =
+ Objects.requireNonNull(mAdSelectionManager.getService());
+ service.removeAdCounterHistogramOverride(
+ new RemoveAdCounterHistogramOverrideInput.Builder()
+ .setAdEventType(removeRequest.getAdEventType())
+ .setAdCounterKey(removeRequest.getAdCounterKey())
+ .setBuyer(removeRequest.getBuyer())
+ .build(),
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> outcomeReceiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ outcomeReceiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service");
+ outcomeReceiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Remote exception encountered while updating ad counter histogram");
+ outcomeReceiver.onError(new IllegalStateException("Failure of AdSelection service", e));
+ }
+ }
+
+ /**
+ * Removes all previously set histogram overrides used in ad selection which were set by the
+ * caller application.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * <p>The given {@code outcomeReceiver} either returns an empty {@link Object} if successful or
+ * an {@link Exception} which indicates the error.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * @hide
+ */
+ // TODO(b/221876775): Unhide for frequency cap API review
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void resetAllAdCounterHistogramOverrides(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> outcomeReceiver) {
+ Objects.requireNonNull(executor, "Executor must not be null");
+ Objects.requireNonNull(outcomeReceiver, "Outcome receiver must not be null");
+
+ try {
+ final AdSelectionService service =
+ Objects.requireNonNull(mAdSelectionManager.getService());
+ service.resetAllAdCounterHistogramOverrides(
+ new AdSelectionOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> outcomeReceiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ outcomeReceiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (NullPointerException e) {
+ sLogger.e(e, "Unable to find the AdSelection service");
+ outcomeReceiver.onError(
+ new IllegalStateException("Unable to find the AdSelection service", e));
+ } catch (RemoteException e) {
+ sLogger.e(e, "Remote exception encountered while updating ad counter histogram");
+ outcomeReceiver.onError(new IllegalStateException("Failure of AdSelection service", e));
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/UpdateAdCounterHistogramInput.java b/android-34/android/adservices/adselection/UpdateAdCounterHistogramInput.java
new file mode 100644
index 0000000..b29a507
--- /dev/null
+++ b/android-34/android/adservices/adselection/UpdateAdCounterHistogramInput.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID;
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID_MESSAGE;
+import static android.adservices.adselection.UpdateAdCounterHistogramRequest.DISALLOW_AD_EVENT_TYPE_WIN_MESSAGE;
+import static android.adservices.adselection.UpdateAdCounterHistogramRequest.UNSET_AD_EVENT_TYPE_MESSAGE;
+import static android.adservices.adselection.UpdateAdCounterHistogramRequest.UNSET_CALLER_ADTECH_MESSAGE;
+import static android.adservices.common.FrequencyCapFilters.AD_EVENT_TYPE_INVALID;
+import static android.adservices.common.FrequencyCapFilters.AD_EVENT_TYPE_WIN;
+
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.FrequencyCapFilters;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Input object wrapping the required arguments needed to update an ad counter histogram.
+ *
+ * <p>The ad counter histograms, which are historical logs of events which are associated with an ad
+ * counter key and an ad event type, are used to inform frequency cap filtering in FLEDGE.
+ *
+ * @hide
+ */
+public final class UpdateAdCounterHistogramInput implements Parcelable {
+ private static final String UNSET_CALLER_PACKAGE_NAME_MESSAGE =
+ "Caller package name must not be null";
+
+ private final long mAdSelectionId;
+ @FrequencyCapFilters.AdEventType private final int mAdEventType;
+ @NonNull private final AdTechIdentifier mCallerAdTech;
+ @NonNull private final String mCallerPackageName;
+
+ @NonNull
+ public static final Creator<UpdateAdCounterHistogramInput> CREATOR =
+ new Creator<UpdateAdCounterHistogramInput>() {
+ @Override
+ public UpdateAdCounterHistogramInput createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ return new UpdateAdCounterHistogramInput(in);
+ }
+
+ @Override
+ public UpdateAdCounterHistogramInput[] newArray(int size) {
+ return new UpdateAdCounterHistogramInput[size];
+ }
+ };
+
+ private UpdateAdCounterHistogramInput(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mAdSelectionId = builder.mAdSelectionId;
+ mAdEventType = builder.mAdEventType;
+ mCallerAdTech = builder.mCallerAdTech;
+ mCallerPackageName = builder.mCallerPackageName;
+ }
+
+ private UpdateAdCounterHistogramInput(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mAdSelectionId = in.readLong();
+ mAdEventType = in.readInt();
+ mCallerAdTech = AdTechIdentifier.CREATOR.createFromParcel(in);
+ mCallerPackageName = in.readString();
+ }
+
+ /**
+ * Gets the ad selection ID with which the rendered ad's events are associated.
+ *
+ * <p>The ad must have been selected from FLEDGE ad selection in the last 24 hours, and the ad
+ * selection call must have been initiated from the same app as the current calling app. Event
+ * histograms for all ad counter keys associated with the ad specified by the ad selection ID
+ * will be updated for the ad event type from {@link #getAdEventType()}, to be used in FLEDGE
+ * frequency cap filtering.
+ */
+ public long getAdSelectionId() {
+ return mAdSelectionId;
+ }
+
+ /**
+ * Gets the {@link android.adservices.common.FrequencyCapFilters.AdEventType} which, along with
+ * an ad's counter keys, identifies which histogram should be updated.
+ *
+ * <p>See {@link android.adservices.common.FrequencyCapFilters.AdEventType} for more
+ * information.
+ */
+ @FrequencyCapFilters.AdEventType
+ public int getAdEventType() {
+ return mAdEventType;
+ }
+
+ /**
+ * Gets the caller adtech entity's {@link AdTechIdentifier}.
+ *
+ * <p>The adtech using this {@link UpdateAdCounterHistogramInput} object must have enrolled with
+ * the Privacy Sandbox and be allowed to act on behalf of the calling app. The specified adtech
+ * is not required to be the same adtech as either the buyer which owns the rendered ad or the
+ * seller which initiated the ad selection associated with the ID returned by {@link
+ * #getAdSelectionId()}.
+ */
+ @NonNull
+ public AdTechIdentifier getCallerAdTech() {
+ return mCallerAdTech;
+ }
+
+ /**
+ * Gets the caller app's package name.
+ *
+ * <p>The package name must match the caller package name for the FLEDGE ad selection
+ * represented by the ID returned by {@link #getAdSelectionId()}.
+ */
+ @NonNull
+ public String getCallerPackageName() {
+ return mCallerPackageName;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mAdSelectionId);
+ dest.writeInt(mAdEventType);
+ mCallerAdTech.writeToParcel(dest, flags);
+ dest.writeString(mCallerPackageName);
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Checks whether the {@link UpdateAdCounterHistogramInput} objects contain the same
+ * information.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof UpdateAdCounterHistogramInput)) return false;
+ UpdateAdCounterHistogramInput that = (UpdateAdCounterHistogramInput) o;
+ return mAdSelectionId == that.mAdSelectionId
+ && mAdEventType == that.mAdEventType
+ && mCallerAdTech.equals(that.mCallerAdTech)
+ && mCallerPackageName.equals(that.mCallerPackageName);
+ }
+
+ /** Returns the hash of the {@link UpdateAdCounterHistogramInput} object's data. */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAdSelectionId, mAdEventType, mCallerAdTech, mCallerPackageName);
+ }
+
+ @Override
+ public String toString() {
+ return "UpdateAdCounterHistogramInput{"
+ + "mAdSelectionId="
+ + mAdSelectionId
+ + ", mAdEventType="
+ + mAdEventType
+ + ", mCallerAdTech="
+ + mCallerAdTech
+ + ", mCallerPackageName='"
+ + mCallerPackageName
+ + '\''
+ + '}';
+ }
+
+ /** Builder for {@link UpdateAdCounterHistogramInput} objects. */
+ public static final class Builder {
+ private long mAdSelectionId = UNSET_AD_SELECTION_ID;
+ @FrequencyCapFilters.AdEventType private int mAdEventType = AD_EVENT_TYPE_INVALID;
+ @Nullable private AdTechIdentifier mCallerAdTech;
+ @Nullable private String mCallerPackageName;
+
+ public Builder() {}
+
+ /**
+ * Gets the ad selection ID with which the rendered ad's events are associated.
+ *
+ * <p>See {@link #getAdSelectionId()} for more information.
+ */
+ @NonNull
+ public Builder setAdSelectionId(long adSelectionId) {
+ mAdSelectionId = adSelectionId;
+ return this;
+ }
+
+ /**
+ * Sets the {@link android.adservices.common.FrequencyCapFilters.AdEventType} which, along
+ * with an ad's counter keys, identifies which histogram should be updated.
+ *
+ * <p>See {@link #getAdEventType()} for more information.
+ */
+ @NonNull
+ public Builder setAdEventType(@FrequencyCapFilters.AdEventType int adEventType) {
+ Preconditions.checkArgument(
+ adEventType != AD_EVENT_TYPE_WIN, DISALLOW_AD_EVENT_TYPE_WIN_MESSAGE);
+ mAdEventType = adEventType;
+ return this;
+ }
+
+ /**
+ * Sets the caller adtech entity's {@link AdTechIdentifier}.
+ *
+ * <p>See {@link #getCallerAdTech()} for more information.
+ */
+ @NonNull
+ public Builder setCallerAdTech(@NonNull AdTechIdentifier callerAdTech) {
+ Objects.requireNonNull(callerAdTech, UNSET_CALLER_ADTECH_MESSAGE);
+ mCallerAdTech = callerAdTech;
+ return this;
+ }
+
+ /**
+ * Sets the caller app's package name.
+ *
+ * <p>See {@link #getCallerPackageName()} for more information.
+ */
+ @NonNull
+ public Builder setCallerPackageName(@NonNull String callerPackageName) {
+ Objects.requireNonNull(callerPackageName, UNSET_CALLER_PACKAGE_NAME_MESSAGE);
+ mCallerPackageName = callerPackageName;
+ return this;
+ }
+
+ /**
+ * Builds the {@link UpdateAdCounterHistogramInput} object.
+ *
+ * @throws NullPointerException if the caller's {@link AdTechIdentifier} or package name are
+ * not set
+ * @throws IllegalArgumentException if the ad selection ID is not set
+ */
+ @NonNull
+ public UpdateAdCounterHistogramInput build()
+ throws NullPointerException, IllegalArgumentException {
+ Preconditions.checkArgument(
+ mAdSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
+ Preconditions.checkArgument(
+ mAdEventType != AD_EVENT_TYPE_INVALID, UNSET_AD_EVENT_TYPE_MESSAGE);
+ Objects.requireNonNull(mCallerAdTech, UNSET_CALLER_ADTECH_MESSAGE);
+ Objects.requireNonNull(mCallerPackageName, UNSET_CALLER_PACKAGE_NAME_MESSAGE);
+
+ return new UpdateAdCounterHistogramInput(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/adselection/UpdateAdCounterHistogramRequest.java b/android-34/android/adservices/adselection/UpdateAdCounterHistogramRequest.java
new file mode 100644
index 0000000..339f812
--- /dev/null
+++ b/android-34/android/adservices/adselection/UpdateAdCounterHistogramRequest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.adselection;
+
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID;
+import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID_MESSAGE;
+import static android.adservices.common.FrequencyCapFilters.AD_EVENT_TYPE_INVALID;
+import static android.adservices.common.FrequencyCapFilters.AD_EVENT_TYPE_WIN;
+
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.FrequencyCapFilters;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.OutcomeReceiver;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Request object wrapping the required arguments needed to update an ad counter histogram.
+ *
+ * <p>The ad counter histograms, which are historical logs of events which are associated with an ad
+ * counter key and an ad event type, are used to inform frequency cap filtering in FLEDGE.
+ *
+ * @hide
+ */
+// TODO(b/221876775): Unhide for frequency cap API review
+public class UpdateAdCounterHistogramRequest {
+ /** @hide */
+ public static final String UNSET_AD_EVENT_TYPE_MESSAGE = "Ad event type must be set";
+
+ /** @hide */
+ public static final String DISALLOW_AD_EVENT_TYPE_WIN_MESSAGE =
+ "Win event types cannot be manually updated";
+
+ /** @hide */
+ public static final String UNSET_CALLER_ADTECH_MESSAGE = "Caller ad tech must not be null";
+
+ private final long mAdSelectionId;
+ @FrequencyCapFilters.AdEventType private final int mAdEventType;
+ @NonNull private final AdTechIdentifier mCallerAdTech;
+
+ private UpdateAdCounterHistogramRequest(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mAdSelectionId = builder.mAdSelectionId;
+ mAdEventType = builder.mAdEventType;
+ mCallerAdTech = builder.mCallerAdTech;
+ }
+
+ /**
+ * Gets the ad selection ID with which the rendered ad's events are associated.
+ *
+ * <p>For more information about the ad selection ID, see {@link AdSelectionOutcome}.
+ *
+ * <p>The ad must have been selected from FLEDGE ad selection in the last 24 hours, and the ad
+ * selection call must have been initiated from the same app as the current calling app. Event
+ * histograms for all ad counter keys associated with the ad specified by the ad selection ID
+ * will be updated for the ad event type from {@link #getAdEventType()}, to be used in FLEDGE
+ * frequency cap filtering.
+ */
+ public long getAdSelectionId() {
+ return mAdSelectionId;
+ }
+
+ /**
+ * Gets the ad event type which, along with an ad's counter keys, identifies which histogram
+ * should be updated.
+ */
+ @FrequencyCapFilters.AdEventType
+ public int getAdEventType() {
+ return mAdEventType;
+ }
+
+ /**
+ * Gets the caller adtech entity's {@link AdTechIdentifier}.
+ *
+ * <p>The adtech using this {@link UpdateAdCounterHistogramRequest} object must have enrolled
+ * with the Privacy Sandbox and be allowed to act on behalf of the calling app. The specified
+ * adtech is not required to be the same adtech as either the buyer which owns the rendered ad
+ * or the seller which initiated the ad selection associated with the ID returned by {@link
+ * #getAdSelectionId()}.
+ *
+ * <p>For more information about API requirements and exceptions, see {@link
+ * AdSelectionManager#updateAdCounterHistogram(UpdateAdCounterHistogramRequest, Executor,
+ * OutcomeReceiver)}.
+ */
+ @NonNull
+ public AdTechIdentifier getCallerAdTech() {
+ return mCallerAdTech;
+ }
+
+ /**
+ * Checks whether the {@link UpdateAdCounterHistogramRequest} objects contain the same
+ * information.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof UpdateAdCounterHistogramRequest)) return false;
+ UpdateAdCounterHistogramRequest that = (UpdateAdCounterHistogramRequest) o;
+ return mAdSelectionId == that.mAdSelectionId
+ && mAdEventType == that.mAdEventType
+ && mCallerAdTech.equals(that.mCallerAdTech);
+ }
+
+ /** Returns the hash of the {@link UpdateAdCounterHistogramRequest} object's data. */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAdSelectionId, mAdEventType, mCallerAdTech);
+ }
+
+ @Override
+ public String toString() {
+ return "UpdateAdCounterHistogramRequest{"
+ + "mAdSelectionId="
+ + mAdSelectionId
+ + ", mAdEventType="
+ + mAdEventType
+ + ", mCallerAdTech="
+ + mCallerAdTech
+ + '}';
+ }
+
+ /** Builder for {@link UpdateAdCounterHistogramRequest} objects. */
+ public static final class Builder {
+ private long mAdSelectionId = UNSET_AD_SELECTION_ID;
+ @FrequencyCapFilters.AdEventType private int mAdEventType = AD_EVENT_TYPE_INVALID;
+ @Nullable private AdTechIdentifier mCallerAdTech;
+
+ public Builder() {}
+
+ /**
+ * Gets the ad selection ID with which the rendered ad's events are associated.
+ *
+ * <p>See {@link #getAdSelectionId()} for more information.
+ */
+ @NonNull
+ public Builder setAdSelectionId(long adSelectionId) {
+ mAdSelectionId = adSelectionId;
+ return this;
+ }
+
+ /**
+ * Sets the ad event type which, along with an ad's counter keys, identifies which histogram
+ * should be updated.
+ *
+ * <p>See {@link #getAdEventType()} for more information.
+ */
+ @NonNull
+ public Builder setAdEventType(@FrequencyCapFilters.AdEventType int adEventType) {
+ Preconditions.checkArgument(
+ adEventType != AD_EVENT_TYPE_WIN, DISALLOW_AD_EVENT_TYPE_WIN_MESSAGE);
+ mAdEventType = adEventType;
+ return this;
+ }
+
+ /**
+ * Sets the caller adtech entity's {@link AdTechIdentifier}.
+ *
+ * <p>See {@link #getCallerAdTech()} for more information.
+ */
+ @NonNull
+ public Builder setCallerAdTech(@NonNull AdTechIdentifier callerAdTech) {
+ Objects.requireNonNull(callerAdTech, UNSET_CALLER_ADTECH_MESSAGE);
+ mCallerAdTech = callerAdTech;
+ return this;
+ }
+
+ /**
+ * Builds the {@link UpdateAdCounterHistogramRequest} object.
+ *
+ * @throws NullPointerException if the caller's {@link AdTechIdentifier} is not set
+ * @throws IllegalArgumentException if the ad selection ID is not set
+ */
+ @NonNull
+ public UpdateAdCounterHistogramRequest build()
+ throws NullPointerException, IllegalArgumentException {
+ Preconditions.checkArgument(
+ mAdSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
+ Preconditions.checkArgument(
+ mAdEventType != AD_EVENT_TYPE_INVALID, UNSET_AD_EVENT_TYPE_MESSAGE);
+ Objects.requireNonNull(mCallerAdTech, UNSET_CALLER_ADTECH_MESSAGE);
+
+ return new UpdateAdCounterHistogramRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/appsetid/AppSetId.java b/android-34/android/adservices/appsetid/AppSetId.java
new file mode 100644
index 0000000..247ad6a
--- /dev/null
+++ b/android-34/android/adservices/appsetid/AppSetId.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.appsetid;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A unique, per-device, per developer-account user-resettable ID for non-monetizing advertising
+ * usecases.
+ *
+ * <p>Represents the appSetID and scope of this appSetId from the {@link
+ * AppSetIdManager#getAppSetId(Executor, OutcomeReceiver)} API. The scope of the ID can be per app
+ * or per developer account associated with the user. AppSetId is used for analytics, spam
+ * detection, frequency capping and fraud prevention use cases, on a given device, that one may need
+ * to correlate usage or actions across a set of apps owned by an organization.
+ */
+public class AppSetId {
+ @NonNull private final String mAppSetId;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SCOPE_APP,
+ SCOPE_DEVELOPER,
+ })
+ public @interface AppSetIdScope {}
+ /** The appSetId is scoped to an app. All apps on a device will have a different appSetId. */
+ public static final int SCOPE_APP = 1;
+
+ /**
+ * The appSetId is scoped to a developer account on an app store. All apps from the same
+ * developer on a device will have the same developer scoped appSetId.
+ */
+ public static final int SCOPE_DEVELOPER = 2;
+
+ private final @AppSetIdScope int mAppSetIdScope;
+
+ /**
+ * Creates an instance of {@link AppSetId}
+ *
+ * @param appSetId generated by the provider service.
+ * @param appSetIdScope scope of the appSetId.
+ */
+ public AppSetId(@NonNull String appSetId, @AppSetIdScope int appSetIdScope) {
+ mAppSetId = appSetId;
+ mAppSetIdScope = appSetIdScope;
+ }
+
+ /** Retrieves the appSetId. The api always returns a non-empty appSetId. */
+ public @NonNull String getId() {
+ return mAppSetId;
+ }
+
+ /** Retrieves the scope of the appSetId. */
+ public @AppSetIdScope int getScope() {
+ return mAppSetIdScope;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof AppSetId)) {
+ return false;
+ }
+ AppSetId that = (AppSetId) o;
+ return mAppSetId.equals(that.mAppSetId) && (mAppSetIdScope == that.mAppSetIdScope);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAppSetId, mAppSetIdScope);
+ }
+}
diff --git a/android-34/android/adservices/appsetid/AppSetIdManager.java b/android-34/android/adservices/appsetid/AppSetIdManager.java
new file mode 100644
index 0000000..1308979
--- /dev/null
+++ b/android-34/android/adservices/appsetid/AppSetIdManager.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.appsetid;
+
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.CallerMetadata;
+import android.adservices.common.SandboxedSdkContextUtils;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.app.sdksandbox.SandboxedSdkContext;
+import android.content.Context;
+import android.os.Build;
+import android.os.LimitExceededException;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.SystemClock;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.LogUtil;
+import com.android.adservices.ServiceBinder;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * AppSetIdManager provides APIs for app and ad-SDKs to access appSetId for non-monetizing purpose.
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public class AppSetIdManager {
+ /**
+ * Service used for registering AppSetIdManager in the system service registry.
+ *
+ * @hide
+ */
+ public static final String APPSETID_SERVICE = "appsetid_service";
+
+ /* When an app calls the AppSetId API directly, it sets the SDK name to empty string. */
+ static final String EMPTY_SDK = "";
+
+ private Context mContext;
+ private ServiceBinder<IAppSetIdService> mServiceBinder;
+
+ /**
+ * Factory method for creating an instance of AppSetIdManager.
+ *
+ * @param context The {@link Context} to use
+ * @return A {@link AppSetIdManager} instance
+ */
+ @NonNull
+ public static AppSetIdManager get(@NonNull Context context) {
+ // On T+, context.getSystemService() does more than just call constructor.
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ ? context.getSystemService(AppSetIdManager.class)
+ : new AppSetIdManager(context);
+ }
+
+ /**
+ * Create AppSetIdManager
+ *
+ * @hide
+ */
+ public AppSetIdManager(Context context) {
+ // In case the AppSetIdManager is initiated from inside a sdk_sandbox process the fields
+ // will be immediately rewritten by the initialize method below.
+ initialize(context);
+ }
+
+ /**
+ * Initializes {@link AppSetIdManager} with the given {@code context}.
+ *
+ * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context.
+ * For more information check the javadoc on the {@link
+ * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}.
+ *
+ * @hide
+ * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry
+ */
+ public AppSetIdManager initialize(Context context) {
+ mContext = context;
+ mServiceBinder =
+ ServiceBinder.getServiceBinder(
+ context,
+ AdServicesCommon.ACTION_APPSETID_SERVICE,
+ IAppSetIdService.Stub::asInterface);
+ return this;
+ }
+
+ @NonNull
+ private IAppSetIdService getService() {
+ IAppSetIdService service = mServiceBinder.getService();
+ if (service == null) {
+ throw new IllegalStateException("Unable to find the service");
+ }
+ return service;
+ }
+
+ @NonNull
+ private Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Retrieve the AppSetId.
+ *
+ * @param executor The executor to run callback.
+ * @param callback The callback that's called after appsetid are available or an error occurs.
+ * @throws SecurityException if caller is not authorized to call this API.
+ * @throws IllegalStateException if this API is not available.
+ * @throws LimitExceededException if rate limit was reached.
+ */
+ @NonNull
+ public void getAppSetId(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<AppSetId, Exception> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ CallerMetadata callerMetadata =
+ new CallerMetadata.Builder()
+ .setBinderElapsedTimestamp(SystemClock.elapsedRealtime())
+ .build();
+ final IAppSetIdService service = getService();
+ String appPackageName = "";
+ String sdkPackageName = "";
+ // First check if context is SandboxedSdkContext or not
+ Context getAppSetIdRequestContext = getContext();
+ SandboxedSdkContext requestContext =
+ SandboxedSdkContextUtils.getAsSandboxedSdkContext(getAppSetIdRequestContext);
+ if (requestContext != null) {
+ sdkPackageName = requestContext.getSdkPackageName();
+ appPackageName = requestContext.getClientPackageName();
+ } else { // This is the case without the Sandbox.
+ appPackageName = getAppSetIdRequestContext.getPackageName();
+ }
+ try {
+ service.getAppSetId(
+ new GetAppSetIdParam.Builder()
+ .setAppPackageName(appPackageName)
+ .setSdkPackageName(sdkPackageName)
+ .build(),
+ callerMetadata,
+ new IGetAppSetIdCallback.Stub() {
+ @Override
+ public void onResult(GetAppSetIdResult resultParcel) {
+ executor.execute(
+ () -> {
+ if (resultParcel.isSuccess()) {
+ callback.onResult(
+ new AppSetId(
+ resultParcel.getAppSetId(),
+ resultParcel.getAppSetIdScope()));
+ } else {
+ callback.onError(
+ AdServicesStatusUtils.asException(
+ resultParcel));
+ }
+ });
+ }
+
+ @Override
+ public void onError(int resultCode) {
+ executor.execute(
+ () ->
+ callback.onError(
+ AdServicesStatusUtils.asException(resultCode)));
+ }
+ });
+ } catch (RemoteException e) {
+ LogUtil.e("RemoteException", e);
+ callback.onError(e);
+ }
+ }
+
+ /**
+ * If the service is in an APK (as opposed to the system service), unbind it from the service to
+ * allow the APK process to die.
+ *
+ * @hide
+ */
+ // TODO: change to @VisibleForTesting
+ public void unbindFromService() {
+ mServiceBinder.unbindFromService();
+ }
+}
diff --git a/android-34/android/adservices/appsetid/AppSetIdProviderService.java b/android-34/android/adservices/appsetid/AppSetIdProviderService.java
new file mode 100644
index 0000000..fbe93fa
--- /dev/null
+++ b/android-34/android/adservices/appsetid/AppSetIdProviderService.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.appsetid;
+
+import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.IOException;
+
+/**
+ * Abstract Base class for provider service to implement generation of AppSetId with appropriate
+ * appSetId scope value.
+ *
+ * <p>The implementor of this service needs to override the onGetAppSetIdProvider method and provide
+ * an app-scoped or developer-account scoped unique appSetId.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class AppSetIdProviderService extends Service {
+
+ /** The intent that the service must respond to. Add it to the intent filter of the service. */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.adservices.appsetid.AppSetIdProviderService";
+
+ /** Abstract method which will be overridden by provider to provide the appsetid. */
+ @NonNull
+ public abstract AppSetId onGetAppSetId(int clientUid, @NonNull String clientPackageName)
+ throws IOException;
+
+ private final android.adservices.appsetid.IAppSetIdProviderService mInterface =
+ new android.adservices.appsetid.IAppSetIdProviderService.Stub() {
+ @Override
+ public void getAppSetId(
+ int appUID,
+ @NonNull String packageName,
+ @NonNull IGetAppSetIdProviderCallback resultCallback)
+ throws RemoteException {
+ try {
+ AppSetId appsetId = onGetAppSetId(appUID, packageName);
+ GetAppSetIdResult appsetIdInternal =
+ new GetAppSetIdResult.Builder()
+ .setStatusCode(STATUS_SUCCESS)
+ .setErrorMessage("")
+ .setAppSetId(appsetId.getId())
+ .setAppSetIdScope(appsetId.getScope())
+ .build();
+
+ resultCallback.onResult(appsetIdInternal);
+ } catch (Throwable e) {
+ resultCallback.onError(e.getMessage());
+ }
+ }
+ };
+
+ @Nullable
+ @Override
+ public final IBinder onBind(@Nullable Intent intent) {
+ return mInterface.asBinder();
+ }
+}
diff --git a/android-34/android/adservices/appsetid/GetAppSetIdParam.java b/android-34/android/adservices/appsetid/GetAppSetIdParam.java
new file mode 100644
index 0000000..af54f52
--- /dev/null
+++ b/android-34/android/adservices/appsetid/GetAppSetIdParam.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.appsetid;
+
+import static android.adservices.appsetid.AppSetIdManager.EMPTY_SDK;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represent input params to the getAppSetId API.
+ *
+ * @hide
+ */
+public final class GetAppSetIdParam implements Parcelable {
+ private final String mSdkPackageName;
+ private final String mAppPackageName;
+
+ private GetAppSetIdParam(@Nullable String sdkPackageName, @NonNull String appPackageName) {
+ mSdkPackageName = sdkPackageName;
+ mAppPackageName = appPackageName;
+ }
+
+ private GetAppSetIdParam(@NonNull Parcel in) {
+ mSdkPackageName = in.readString();
+ mAppPackageName = in.readString();
+ }
+
+ public static final @NonNull Creator<GetAppSetIdParam> CREATOR =
+ new Parcelable.Creator<GetAppSetIdParam>() {
+ @Override
+ public GetAppSetIdParam createFromParcel(Parcel in) {
+ return new GetAppSetIdParam(in);
+ }
+
+ @Override
+ public GetAppSetIdParam[] newArray(int size) {
+ return new GetAppSetIdParam[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeString(mSdkPackageName);
+ out.writeString(mAppPackageName);
+ }
+
+ /** Get the Sdk Package Name. This is the package name in the Manifest. */
+ @NonNull
+ public String getSdkPackageName() {
+ return mSdkPackageName;
+ }
+
+ /** Get the App PackageName. */
+ @NonNull
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /** Builder for {@link GetAppSetIdParam} objects. */
+ public static final class Builder {
+ private String mSdkPackageName;
+ private String mAppPackageName;
+
+ public Builder() {}
+
+ /**
+ * Set the Sdk Package Name. When the app calls the AppSetId API directly without using an
+ * SDK, don't set this field.
+ */
+ public @NonNull Builder setSdkPackageName(@NonNull String sdkPackageName) {
+ mSdkPackageName = sdkPackageName;
+ return this;
+ }
+
+ /** Set the App PackageName. */
+ public @NonNull Builder setAppPackageName(@NonNull String appPackageName) {
+ mAppPackageName = appPackageName;
+ return this;
+ }
+
+ /** Builds a {@link GetAppSetIdParam} instance. */
+ public @NonNull GetAppSetIdParam build() {
+ if (mSdkPackageName == null) {
+ // When Sdk package name is not set, we assume the App calls the AppSetId API
+ // directly.
+ // We set the Sdk package name to empty to mark this.
+ mSdkPackageName = EMPTY_SDK;
+ }
+
+ if (mAppPackageName == null || mAppPackageName.isEmpty()) {
+ throw new IllegalArgumentException("App PackageName must not be empty or null");
+ }
+
+ return new GetAppSetIdParam(mSdkPackageName, mAppPackageName);
+ }
+ }
+}
diff --git a/android-34/android/adservices/appsetid/GetAppSetIdResult.java b/android-34/android/adservices/appsetid/GetAppSetIdResult.java
new file mode 100644
index 0000000..9a750a6
--- /dev/null
+++ b/android-34/android/adservices/appsetid/GetAppSetIdResult.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.appsetid;
+
+import android.adservices.common.AdServicesResponse;
+import android.adservices.common.AdServicesStatusUtils;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represent the result from the getAppSetId API.
+ *
+ * @hide
+ */
+public final class GetAppSetIdResult extends AdServicesResponse {
+ @NonNull private final String mAppSetId;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SCOPE_APP,
+ SCOPE_DEVELOPER,
+ })
+ public @interface AppSetIdScope {}
+ /** The appSetId is scoped to an app. All apps on a device will have a different appSetId. */
+ public static final int SCOPE_APP = 1;
+
+ /**
+ * The appSetId is scoped to a developer account on an app store. All apps from the same
+ * developer on a device will have the same developer scoped appSetId.
+ */
+ public static final int SCOPE_DEVELOPER = 2;
+
+ private final @AppSetIdScope int mAppSetIdScope;
+
+ private GetAppSetIdResult(
+ @AdServicesStatusUtils.StatusCode int resultCode,
+ @Nullable String errorMessage,
+ @NonNull String appSetId,
+ @AppSetIdScope int appSetIdScope) {
+ super(resultCode, errorMessage);
+ mAppSetId = appSetId;
+ mAppSetIdScope = appSetIdScope;
+ }
+
+ private GetAppSetIdResult(@NonNull Parcel in) {
+ super(in);
+ Objects.requireNonNull(in);
+
+ mAppSetId = in.readString();
+ mAppSetIdScope = in.readInt();
+ }
+
+ public static final @NonNull Creator<GetAppSetIdResult> CREATOR =
+ new Parcelable.Creator<GetAppSetIdResult>() {
+ @Override
+ public GetAppSetIdResult createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new GetAppSetIdResult(in);
+ }
+
+ @Override
+ public GetAppSetIdResult[] newArray(int size) {
+ return new GetAppSetIdResult[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mStatusCode);
+ out.writeString(mErrorMessage);
+ out.writeString(mAppSetId);
+ out.writeInt(mAppSetIdScope);
+ }
+
+ /**
+ * Returns the error message associated with this result.
+ *
+ * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. The error
+ * message may be {@code null} even if {@link #isSuccess} is {@code false}.
+ */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /** Returns the AppSetId associated with this result. */
+ @NonNull
+ public String getAppSetId() {
+ return mAppSetId;
+ }
+
+ /** Returns the AppSetId scope associated with this result. */
+ public @AppSetIdScope int getAppSetIdScope() {
+ return mAppSetIdScope;
+ }
+
+ @Override
+ public String toString() {
+ return "GetAppSetIdResult{"
+ + "mResultCode="
+ + mStatusCode
+ + ", mErrorMessage='"
+ + mErrorMessage
+ + '\''
+ + ", mAppSetId="
+ + mAppSetId
+ + ", mAppSetIdScope="
+ + mAppSetIdScope
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof GetAppSetIdResult)) {
+ return false;
+ }
+
+ GetAppSetIdResult that = (GetAppSetIdResult) o;
+
+ return mStatusCode == that.mStatusCode
+ && Objects.equals(mErrorMessage, that.mErrorMessage)
+ && Objects.equals(mAppSetId, that.mAppSetId)
+ && (mAppSetIdScope == that.mAppSetIdScope);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mStatusCode, mErrorMessage, mAppSetId, mAppSetIdScope);
+ }
+
+ /**
+ * Builder for {@link GetAppSetIdResult} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ private @AdServicesStatusUtils.StatusCode int mStatusCode;
+ @Nullable private String mErrorMessage;
+ @NonNull private String mAppSetId;
+ private @AppSetIdScope int mAppSetIdScope;
+
+ public Builder() {}
+
+ /** Set the Result Code. */
+ public @NonNull Builder setStatusCode(@AdServicesStatusUtils.StatusCode int statusCode) {
+ mStatusCode = statusCode;
+ return this;
+ }
+
+ /** Set the Error Message. */
+ public @NonNull Builder setErrorMessage(@Nullable String errorMessage) {
+ mErrorMessage = errorMessage;
+ return this;
+ }
+
+ /** Set the appSetId. */
+ public @NonNull Builder setAppSetId(@NonNull String appSetId) {
+ mAppSetId = appSetId;
+ return this;
+ }
+
+ /** Set the appSetId scope field. */
+ public @NonNull Builder setAppSetIdScope(@AppSetIdScope int scope) {
+ mAppSetIdScope = scope;
+ return this;
+ }
+
+ /** Builds a {@link GetAppSetIdResult} instance. */
+ public @NonNull GetAppSetIdResult build() {
+ if (mAppSetId == null) {
+ throw new IllegalArgumentException("appSetId is null");
+ }
+
+ return new GetAppSetIdResult(mStatusCode, mErrorMessage, mAppSetId, mAppSetIdScope);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/AdData.java b/android-34/android/adservices/common/AdData.java
new file mode 100644
index 0000000..6851cc5
--- /dev/null
+++ b/android-34/android/adservices/common/AdData.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/** Represents data specific to an ad that is necessary for ad selection and rendering. */
+public final class AdData implements Parcelable {
+ @NonNull private final Uri mRenderUri;
+ @NonNull private final String mMetadata;
+ @NonNull private final Set<String> mAdCounterKeys;
+ @Nullable private final AdFilters mAdFilters;
+
+ @NonNull
+ public static final Creator<AdData> CREATOR =
+ new Creator<AdData>() {
+ @Override
+ public AdData createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ return new AdData(in);
+ }
+
+ @Override
+ public AdData[] newArray(int size) {
+ return new AdData[size];
+ }
+ };
+
+ private AdData(@NonNull AdData.Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mRenderUri = builder.mRenderUri;
+ mMetadata = builder.mMetadata;
+ mAdCounterKeys = builder.mAdCounterKeys;
+ mAdFilters = builder.mAdFilters;
+ }
+
+ private AdData(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mRenderUri = Uri.CREATOR.createFromParcel(in);
+ mMetadata = in.readString();
+ mAdCounterKeys =
+ AdServicesParcelableUtil.readNullableFromParcel(
+ in, AdServicesParcelableUtil::readStringSetFromParcel);
+ mAdFilters =
+ AdServicesParcelableUtil.readNullableFromParcel(
+ in, AdFilters.CREATOR::createFromParcel);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ mRenderUri.writeToParcel(dest, flags);
+ dest.writeString(mMetadata);
+ AdServicesParcelableUtil.writeNullableToParcel(
+ dest, mAdCounterKeys, AdServicesParcelableUtil::writeStringSetToParcel);
+ AdServicesParcelableUtil.writeNullableToParcel(
+ dest,
+ mAdFilters,
+ (targetParcel, sourceFilters) -> sourceFilters.writeToParcel(targetParcel, flags));
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Gets the URI that points to the ad's rendering assets. The URI must use HTTPS. */
+ @NonNull
+ public Uri getRenderUri() {
+ return mRenderUri;
+ }
+
+ /**
+ * Gets the buyer ad metadata used during the ad selection process.
+ *
+ * <p>The metadata should be a valid JSON object serialized as a string. Metadata represents
+ * ad-specific bidding information that will be used during ad selection as part of bid
+ * generation and used in buyer JavaScript logic, which is executed in an isolated execution
+ * environment.
+ *
+ * <p>If the metadata is not a valid JSON object that can be consumed by the buyer's JS, the ad
+ * will not be eligible for ad selection.
+ */
+ @NonNull
+ public String getMetadata() {
+ return mMetadata;
+ }
+
+ /**
+ * Gets the set of keys used in counting events.
+ *
+ * <p>The keys and counts per key are used in frequency cap filtering during ad selection to
+ * disqualify associated ads from being submitted to bidding.
+ *
+ * <p>Note that these keys can be overwritten along with the ads and other bidding data for a
+ * custom audience during the custom audience's daily update.
+ *
+ * @hide
+ */
+ // TODO(b/221876775): Unhide for frequency cap API review
+ @NonNull
+ public Set<String> getAdCounterKeys() {
+ return mAdCounterKeys;
+ }
+
+ /**
+ * Gets all {@link AdFilters} associated with the ad.
+ *
+ * <p>The filters, if met or exceeded, exclude the associated ad from participating in ad
+ * selection. They are optional and if {@code null} specify that no filters apply to this ad.
+ *
+ * @hide
+ */
+ // TODO(b/221876775): Unhide for app install/frequency cap API review
+ @Nullable
+ public AdFilters getAdFilters() {
+ return mAdFilters;
+ }
+
+ /** Checks whether two {@link AdData} objects contain the same information. */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AdData)) return false;
+ AdData adData = (AdData) o;
+ return mRenderUri.equals(adData.mRenderUri)
+ && mMetadata.equals(adData.mMetadata)
+ && mAdCounterKeys.equals(adData.mAdCounterKeys)
+ && Objects.equals(mAdFilters, adData.mAdFilters);
+ }
+
+ /** Returns the hash of the {@link AdData} object's data. */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRenderUri, mMetadata, mAdCounterKeys, mAdFilters);
+ }
+
+ @Override
+ public String toString() {
+ return "AdData{"
+ + "mRenderUri="
+ + mRenderUri
+ + ", mMetadata='"
+ + mMetadata
+ + '\''
+ + generateAdCounterKeyString()
+ + generateAdFilterString()
+ + '}';
+ }
+
+ private String generateAdCounterKeyString() {
+ // TODO(b/221876775) Add ad counter keys String when unhidden
+ return "";
+ }
+
+ private String generateAdFilterString() {
+ // TODO(b/266837113) Add ad filters String when unhidden
+ return "";
+ }
+
+ /** Builder for {@link AdData} objects. */
+ public static final class Builder {
+ @Nullable private Uri mRenderUri;
+ @Nullable private String mMetadata;
+ @NonNull private Set<String> mAdCounterKeys = new HashSet<>();
+ @Nullable private AdFilters mAdFilters;
+
+ // TODO(b/232883403): We may need to add @NonNUll members as args.
+ public Builder() {}
+
+ /**
+ * Sets the URI that points to the ad's rendering assets. The URI must use HTTPS.
+ *
+ * <p>See {@link #getRenderUri()} for detail.
+ */
+ @NonNull
+ public AdData.Builder setRenderUri(@NonNull Uri renderUri) {
+ Objects.requireNonNull(renderUri);
+ mRenderUri = renderUri;
+ return this;
+ }
+
+ /**
+ * Sets the buyer ad metadata used during the ad selection process.
+ *
+ * <p>The metadata should be a valid JSON object serialized as a string. Metadata represents
+ * ad-specific bidding information that will be used during ad selection as part of bid
+ * generation and used in buyer JavaScript logic, which is executed in an isolated execution
+ * environment.
+ *
+ * <p>If the metadata is not a valid JSON object that can be consumed by the buyer's JS, the
+ * ad will not be eligible for ad selection.
+ *
+ * <p>See {@link #getMetadata()} for detail.
+ */
+ @NonNull
+ public AdData.Builder setMetadata(@NonNull String metadata) {
+ Objects.requireNonNull(metadata);
+ mMetadata = metadata;
+ return this;
+ }
+
+ /**
+ * Sets the set of keys used in counting events.
+ *
+ * <p>See {@link #getAdCounterKeys()} for more information.
+ *
+ * @hide
+ */
+ // TODO(b/221876775): Unhide for frequency cap API review
+ @NonNull
+ public AdData.Builder setAdCounterKeys(@NonNull Set<String> adCounterKeys) {
+ Objects.requireNonNull(adCounterKeys);
+ mAdCounterKeys = adCounterKeys;
+ return this;
+ }
+
+ /**
+ * Sets all {@link AdFilters} associated with the ad.
+ *
+ * <p>See {@link #getAdFilters()} for more information.
+ *
+ * @hide
+ */
+ // TODO(b/221876775): Unhide for app install/frequency cap API review
+ @NonNull
+ public AdData.Builder setAdFilters(@Nullable AdFilters adFilters) {
+ mAdFilters = adFilters;
+ return this;
+ }
+
+ /**
+ * Builds the {@link AdData} object.
+ *
+ * @throws NullPointerException if any required parameters are {@code null} when built
+ */
+ @NonNull
+ public AdData build() {
+ Objects.requireNonNull(mRenderUri);
+ // TODO(b/231997523): Add JSON field validation.
+ Objects.requireNonNull(mMetadata);
+
+ return new AdData(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/AdFilters.java b/android-34/android/adservices/common/AdFilters.java
new file mode 100644
index 0000000..72eb816
--- /dev/null
+++ b/android-34/android/adservices/common/AdFilters.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Objects;
+
+/**
+ * A container class for filters which are associated with an ad.
+ *
+ * <p>If any of the filters in an {@link AdFilters} instance are not satisfied, the associated ad
+ * will not be eligible for ad selection. Filters are optional ad parameters and are not required as
+ * part of {@link AdData}.
+ *
+ * @hide
+ */
+// TODO(b/221876775): Unhide for frequency cap API review
+public final class AdFilters implements Parcelable {
+ /** @hide */
+ @VisibleForTesting public static final String FREQUENCY_CAP_FIELD_NAME = "frequency_cap";
+ /** @hide */
+ @VisibleForTesting public static final String APP_INSTALL_FIELD_NAME = "app_install";
+ /** @hide */
+ @Nullable private final FrequencyCapFilters mFrequencyCapFilters;
+
+ @Nullable private final AppInstallFilters mAppInstallFilters;
+
+ @NonNull
+ public static final Creator<AdFilters> CREATOR =
+ new Creator<AdFilters>() {
+ @Override
+ public AdFilters createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new AdFilters(in);
+ }
+
+ @Override
+ public AdFilters[] newArray(int size) {
+ return new AdFilters[size];
+ }
+ };
+
+ private AdFilters(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mFrequencyCapFilters = builder.mFrequencyCapFilters;
+ mAppInstallFilters = builder.mAppInstallFilters;
+ }
+
+ private AdFilters(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mFrequencyCapFilters =
+ AdServicesParcelableUtil.readNullableFromParcel(
+ in, FrequencyCapFilters.CREATOR::createFromParcel);
+ mAppInstallFilters =
+ AdServicesParcelableUtil.readNullableFromParcel(
+ in, AppInstallFilters.CREATOR::createFromParcel);
+ }
+
+ /**
+ * Gets the {@link FrequencyCapFilters} instance that represents all frequency cap filters for
+ * the ad.
+ *
+ * <p>If {@code null}, there are no frequency cap filters which apply to the ad.
+ *
+ * @hide
+ */
+ @Nullable
+ public FrequencyCapFilters getFrequencyCapFilters() {
+ return mFrequencyCapFilters;
+ }
+
+ /**
+ * Gets the {@link AppInstallFilters} instance that represents all app install filters for the
+ * ad.
+ *
+ * <p>If {@code null}, there are no app install filters which apply to the ad.
+ *
+ * @hide
+ */
+ @Nullable
+ public AppInstallFilters getAppInstallFilters() {
+ return mAppInstallFilters;
+ }
+
+ /**
+ * @return The estimated size of this object, in bytes.
+ * @hide
+ */
+ public int getSizeInBytes() {
+ int size = 0;
+ if (mFrequencyCapFilters != null) {
+ size += mFrequencyCapFilters.getSizeInBytes();
+ }
+ if (mAppInstallFilters != null) {
+ size += mAppInstallFilters.getSizeInBytes();
+ }
+ return size;
+ }
+
+ /**
+ * A JSON serializer.
+ *
+ * @return A JSON serialization of this object.
+ * @hide
+ */
+ public JSONObject toJson() throws JSONException {
+ JSONObject toReturn = new JSONObject();
+ if (mFrequencyCapFilters != null) {
+ toReturn.put(FREQUENCY_CAP_FIELD_NAME, mFrequencyCapFilters.toJson());
+ }
+ if (mAppInstallFilters != null) {
+ toReturn.put(APP_INSTALL_FIELD_NAME, mAppInstallFilters.toJson());
+ }
+ return toReturn;
+ }
+
+ /**
+ * A JSON de-serializer.
+ *
+ * @param json A JSON representation of an {@link AdFilters} object as would be generated by
+ * {@link #toJson()}.
+ * @return An {@link AdFilters} object generated from the given JSON.
+ * @hide
+ */
+ public static AdFilters fromJson(JSONObject json) throws JSONException {
+ Builder builder = new Builder();
+ if (json.has(FREQUENCY_CAP_FIELD_NAME)) {
+ builder.setFrequencyCapFilters(
+ FrequencyCapFilters.fromJson(json.getJSONObject(FREQUENCY_CAP_FIELD_NAME)));
+ }
+ if (json.has(APP_INSTALL_FIELD_NAME)) {
+ builder.setAppInstallFilters(
+ AppInstallFilters.fromJson(json.getJSONObject(APP_INSTALL_FIELD_NAME)));
+ }
+ return builder.build();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ AdServicesParcelableUtil.writeNullableToParcel(
+ dest,
+ mFrequencyCapFilters,
+ (targetParcel, sourceFilters) -> sourceFilters.writeToParcel(targetParcel, flags));
+ AdServicesParcelableUtil.writeNullableToParcel(
+ dest,
+ mAppInstallFilters,
+ (targetParcel, sourceFilters) -> sourceFilters.writeToParcel(targetParcel, flags));
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Checks whether the {@link AdFilters} objects represent the same set of filters. */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AdFilters)) return false;
+ AdFilters adFilters = (AdFilters) o;
+ return Objects.equals(mFrequencyCapFilters, adFilters.mFrequencyCapFilters)
+ && Objects.equals(mAppInstallFilters, adFilters.mAppInstallFilters);
+ }
+
+ /** Returns the hash of the {@link AdFilters} object's data. */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mFrequencyCapFilters, mAppInstallFilters);
+ }
+
+ @Override
+ public String toString() {
+ return "AdFilters{" + generateFrequencyCapString() + generateAppInstallString() + "}";
+ }
+
+ private String generateFrequencyCapString() {
+ // TODO(b/221876775) Add fcap once it is unhidden
+ return "";
+ }
+
+ private String generateAppInstallString() {
+ // TODO(b/266837113) Add app install once it is unhidden
+ return "";
+ }
+
+ /** Builder for creating {@link AdFilters} objects. */
+ public static final class Builder {
+ @Nullable private FrequencyCapFilters mFrequencyCapFilters;
+ @Nullable private AppInstallFilters mAppInstallFilters;
+
+ public Builder() {}
+
+ /**
+ * Sets the {@link FrequencyCapFilters} which will apply to the ad.
+ *
+ * <p>If set to {@code null} or not set, no frequency cap filters will be associated with
+ * the ad.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setFrequencyCapFilters(@Nullable FrequencyCapFilters frequencyCapFilters) {
+ mFrequencyCapFilters = frequencyCapFilters;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AppInstallFilters} which will apply to the ad.
+ *
+ * <p>If set to {@code null} or not set, no app install filters will be associated with the
+ * ad.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setAppInstallFilters(@Nullable AppInstallFilters appInstallFilters) {
+ mAppInstallFilters = appInstallFilters;
+ return this;
+ }
+
+ /** Builds and returns an {@link AdFilters} instance. */
+ @NonNull
+ public AdFilters build() {
+ return new AdFilters(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/AdSelectionSignals.java b/android-34/android/adservices/common/AdSelectionSignals.java
new file mode 100644
index 0000000..5b762ee
--- /dev/null
+++ b/android-34/android/adservices/common/AdSelectionSignals.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * This class holds JSON that will be passed into a JavaScript function during ad selection. Its
+ * contents are not used by <a
+ * href="https://developer.android.com/design-for-safety/privacy-sandbox/fledge">FLEDGE</a> platform
+ * code, but are merely validated and then passed to the appropriate JavaScript ad selection
+ * function.
+ */
+public final class AdSelectionSignals implements Parcelable {
+
+ public static final AdSelectionSignals EMPTY = fromString("{}");
+
+ @NonNull private final String mSignals;
+
+ private AdSelectionSignals(@NonNull Parcel in) {
+ this(in.readString());
+ }
+
+ private AdSelectionSignals(@NonNull String adSelectionSignals) {
+ this(adSelectionSignals, true);
+ }
+
+ private AdSelectionSignals(@NonNull String adSelectionSignals, boolean validate) {
+ Objects.requireNonNull(adSelectionSignals);
+ if (validate) {
+ validate(adSelectionSignals);
+ }
+ mSignals = adSelectionSignals;
+ }
+
+ @NonNull
+ public static final Creator<AdSelectionSignals> CREATOR =
+ new Creator<AdSelectionSignals>() {
+ @Override
+ public AdSelectionSignals createFromParcel(Parcel in) {
+ Objects.requireNonNull(in);
+ return new AdSelectionSignals(in);
+ }
+
+ @Override
+ public AdSelectionSignals[] newArray(int size) {
+ return new AdSelectionSignals[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mSignals);
+ }
+
+ /**
+ * Compares this AdSelectionSignals to the specified object. The result is true if and only if
+ * the argument is not null and is a AdSelectionSignals object with the same string form
+ * (obtained by calling {@link #toString()}). Note that this method will not perform any JSON
+ * normalization so two AdSelectionSignals objects with the same JSON could be not equal if the
+ * String representations of the objects was not equal.
+ *
+ * @param o The object to compare this AdSelectionSignals against
+ * @return true if the given object represents an AdSelectionSignals equivalent to this
+ * AdSelectionSignals, false otherwise
+ */
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof AdSelectionSignals
+ && mSignals.equals(((AdSelectionSignals) o).toString());
+ }
+
+ /**
+ * Returns a hash code corresponding to the string representation of this class obtained by
+ * calling {@link #toString()}. Note that this method will not perform any JSON normalization so
+ * two AdSelectionSignals objects with the same JSON could have different hash codes if the
+ * underlying string representation was different.
+ *
+ * @return a hash code value for this object.
+ */
+ @Override
+ public int hashCode() {
+ return mSignals.hashCode();
+ }
+
+ /** @return The String form of the JSON wrapped by this class. */
+ @Override
+ @NonNull
+ public String toString() {
+ return mSignals;
+ }
+
+ /**
+ * Creates an AdSelectionSignals from a given JSON in String form.
+ *
+ * @param source Any valid JSON string to create the AdSelectionSignals with.
+ * @return An AdSelectionSignals object wrapping the given String.
+ */
+ @NonNull
+ public static AdSelectionSignals fromString(@NonNull String source) {
+ return new AdSelectionSignals(source, true);
+ }
+
+ /**
+ * Creates an AdSelectionSignals from a given JSON in String form.
+ *
+ * @param source Any valid JSON string to create the AdSelectionSignals with.
+ * @param validate Construction-time validation is run on the string if and only if this is
+ * true.
+ * @return An AdSelectionSignals object wrapping the given String.
+ * @hide
+ */
+ @NonNull
+ public static AdSelectionSignals fromString(@NonNull String source, boolean validate) {
+ return new AdSelectionSignals(source, validate);
+ }
+
+ /**
+ * @return the signal's String form data size in bytes.
+ * @hide
+ */
+ public int getSizeInBytes() {
+ return this.mSignals.getBytes().length;
+ }
+
+ private void validate(String inputString) {
+ // TODO(b/238849930) Bring the existing validation function in here
+ }
+}
diff --git a/android-34/android/adservices/common/AdServicesCommonManager.java b/android-34/android/adservices/common/AdServicesCommonManager.java
new file mode 100644
index 0000000..8d3d65a
--- /dev/null
+++ b/android-34/android/adservices/common/AdServicesCommonManager.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_STATE;
+import static android.adservices.common.AdServicesPermissions.MODIFY_ADSERVICES_STATE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.LogUtil;
+import com.android.adservices.ServiceBinder;
+
+import java.util.concurrent.Executor;
+
+/**
+ * AdServicesCommonManager contains APIs common across the various AdServices. It provides two
+ * SystemApis:
+ *
+ * <ul>
+ * <li>isAdServicesEnabled - allows to get AdServices state.
+ * <li>setAdServicesEntryPointEnabled - allows to control AdServices state.
+ * </ul>
+ *
+ * <p>The instance of the {@link AdServicesCommonManager} can be obtained using {@link
+ * Context#getSystemService} and {@link AdServicesCommonManager} class.
+ *
+ * @hide
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+@SystemApi
+public class AdServicesCommonManager {
+ /** @hide */
+ public static final String AD_SERVICES_COMMON_SERVICE = "ad_services_common_service";
+
+ private final Context mContext;
+ private final ServiceBinder<IAdServicesCommonService>
+ mAdServicesCommonServiceBinder;
+
+ /**
+ * Factory method for creating an instance of AdServicesCommonManager.
+ *
+ * @param context The {@link Context} to use
+ * @return A {@link AdServicesCommonManager} instance
+ */
+ @NonNull
+ public static AdServicesCommonManager get(@NonNull Context context) {
+ // On T+, context.getSystemService() does more than just call constructor.
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ ? context.getSystemService(AdServicesCommonManager.class)
+ : new AdServicesCommonManager(context);
+ }
+
+ /**
+ * Create AdServicesCommonManager.
+ *
+ * @hide
+ */
+ public AdServicesCommonManager(@NonNull Context context) {
+ mContext = context;
+ mAdServicesCommonServiceBinder = ServiceBinder.getServiceBinder(
+ context,
+ AdServicesCommon.ACTION_AD_SERVICES_COMMON_SERVICE,
+ IAdServicesCommonService.Stub::asInterface);
+ }
+
+ @NonNull
+ private IAdServicesCommonService getService() {
+ IAdServicesCommonService service =
+ mAdServicesCommonServiceBinder.getService();
+ if (service == null) {
+ throw new IllegalStateException("Unable to find the service");
+ }
+ return service;
+ }
+
+ /**
+ * Get the AdService's enablement state which represents whether AdServices feature is enabled
+ * or not.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(ACCESS_ADSERVICES_STATE)
+ public void isAdServicesEnabled(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Boolean, Exception> callback) {
+ final IAdServicesCommonService service = getService();
+ try {
+ service.isAdServicesEnabled(
+ new IAdServicesCommonCallback.Stub() {
+ @Override
+ public void onResult(IsAdServicesEnabledResult result) {
+ executor.execute(
+ () -> {
+ callback.onResult(result.getAdServicesEnabled());
+ });
+ }
+
+ @Override
+ public void onFailure(int statusCode) {
+ executor.execute(
+ () ->
+ callback.onError(
+ AdServicesStatusUtils.asException(statusCode)));
+ }
+ });
+ } catch (RemoteException e) {
+ LogUtil.e(e, "RemoteException");
+ executor.execute(
+ () -> callback.onError(new IllegalStateException("Internal Error!", e)));
+ }
+ }
+
+ /**
+ * Sets the AdService's enablement state based on the provided parameters.
+ *
+ * <p>As a result of the AdServices state, {@code adServicesEntryPointEnabled}, {@code
+ * adIdEnabled}, appropriate notification may be displayed to the user. It's displayed only once
+ * when all the following conditions are met:
+ *
+ * <ul>
+ * <li>AdServices state - enabled.
+ * <li>adServicesEntryPointEnabled - true.
+ * </ul>
+ *
+ * @param adServicesEntryPointEnabled indicate entry point enabled or not
+ * @param adIdEnabled indicate user opt-out of adid or not
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(MODIFY_ADSERVICES_STATE)
+ public void setAdServicesEnabled(boolean adServicesEntryPointEnabled, boolean adIdEnabled) {
+ final IAdServicesCommonService service = getService();
+ try {
+ service.setAdServicesEnabled(adServicesEntryPointEnabled, adIdEnabled);
+ } catch (RemoteException e) {
+ LogUtil.e(e, "RemoteException");
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/AdServicesPermissions.java b/android-34/android/adservices/common/AdServicesPermissions.java
new file mode 100644
index 0000000..d3eddcf
--- /dev/null
+++ b/android-34/android/adservices/common/AdServicesPermissions.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.SystemApi;
+
+/** Permissions used by the AdServices APIs. */
+public class AdServicesPermissions {
+ private AdServicesPermissions() {}
+
+ /** This permission needs to be declared by the caller of Topics APIs. */
+ public static final String ACCESS_ADSERVICES_TOPICS =
+ "android.permission.ACCESS_ADSERVICES_TOPICS";
+
+ /** This permission needs to be declared by the caller of Attribution APIs. */
+ public static final String ACCESS_ADSERVICES_ATTRIBUTION =
+ "android.permission.ACCESS_ADSERVICES_ATTRIBUTION";
+
+ /** This permission needs to be declared by the caller of Custom Audiences APIs. */
+ public static final String ACCESS_ADSERVICES_CUSTOM_AUDIENCE =
+ "android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCE";
+
+ /** This permission needs to be declared by the caller of Advertising ID APIs. */
+ public static final String ACCESS_ADSERVICES_AD_ID =
+ "android.permission.ACCESS_ADSERVICES_AD_ID";
+
+ /**
+ * This is a signature permission that needs to be declared by the AdServices apk to access API
+ * for AdID provided by another provider service. The signature permission is required to make
+ * sure that only AdServices is permitted to access this api.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ACCESS_PRIVILEGED_AD_ID =
+ "android.permission.ACCESS_PRIVILEGED_AD_ID";
+
+ /**
+ * This is a signature permission needs to be declared by the AdServices apk to access API for
+ * AppSetId provided by another provider service. The signature permission is required to make
+ * sure that only AdServices is permitted to access this api.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ACCESS_PRIVILEGED_APP_SET_ID =
+ "android.permission.ACCESS_PRIVILEGED_APP_SET_ID";
+
+ /**
+ * The permission that lets it modify AdService's enablement state modification API.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String MODIFY_ADSERVICES_STATE =
+ "android.permission.MODIFY_ADSERVICES_STATE";
+
+ /**
+ * The permission that lets it access AdService's enablement state modification API.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ACCESS_ADSERVICES_STATE =
+ "android.permission.ACCESS_ADSERVICES_STATE";
+
+ /**
+ * The permission needed to call AdServicesManager APIs
+ *
+ * @hide
+ */
+ public static final String ACCESS_ADSERVICES_MANAGER =
+ "android.permission.ACCESS_ADSERVICES_MANAGER";
+}
diff --git a/android-34/android/adservices/common/AdServicesResponse.java b/android-34/android/adservices/common/AdServicesResponse.java
new file mode 100644
index 0000000..017fbbe
--- /dev/null
+++ b/android-34/android/adservices/common/AdServicesResponse.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
+import static android.adservices.common.AdServicesStatusUtils.StatusCode;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represents an abstract, generic response for AdServices APIs.
+ *
+ * @hide
+ */
+public class AdServicesResponse implements Parcelable {
+ @NonNull
+ public static final Creator<AdServicesResponse> CREATOR =
+ new Parcelable.Creator<AdServicesResponse>() {
+ @Override
+ public AdServicesResponse createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new AdServicesResponse(in);
+ }
+
+ @Override
+ public AdServicesResponse[] newArray(int size) {
+ return new AdServicesResponse[size];
+ }
+ };
+
+ @StatusCode protected final int mStatusCode;
+ @Nullable protected final String mErrorMessage;
+
+ protected AdServicesResponse(@NonNull Builder builder) {
+ mStatusCode = builder.mStatusCode;
+ mErrorMessage = builder.mErrorMessage;
+ }
+
+ protected AdServicesResponse(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mStatusCode = in.readInt();
+ mErrorMessage = in.readString();
+ }
+
+ protected AdServicesResponse(@StatusCode int statusCode, @Nullable String errorMessage) {
+ mStatusCode = statusCode;
+ mErrorMessage = errorMessage;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ dest.writeInt(mStatusCode);
+ dest.writeString(mErrorMessage);
+ }
+
+ /** Returns one of the {@code STATUS} constants defined in {@link StatusCode}. */
+ @StatusCode
+ public int getStatusCode() {
+ return mStatusCode;
+ }
+
+ /**
+ * Returns {@code true} if {@link #getStatusCode} is {@link
+ * AdServicesStatusUtils#STATUS_SUCCESS}.
+ */
+ public boolean isSuccess() {
+ return getStatusCode() == STATUS_SUCCESS;
+ }
+
+ /** Returns the error message associated with this response. */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /**
+ * Builder for {@link AdServicesResponse} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @StatusCode private int mStatusCode = STATUS_SUCCESS;
+ @Nullable private String mErrorMessage;
+
+ public Builder() {}
+
+ /** Set the Status Code. */
+ @NonNull
+ public AdServicesResponse.Builder setStatusCode(@StatusCode int statusCode) {
+ mStatusCode = statusCode;
+ return this;
+ }
+
+ /** Set the Error Message. */
+ @NonNull
+ public AdServicesResponse.Builder setErrorMessage(@Nullable String errorMessage) {
+ mErrorMessage = errorMessage;
+ return this;
+ }
+
+ /** Builds a {@link AdServicesResponse} instance. */
+ @NonNull
+ public AdServicesResponse build() {
+ return new AdServicesResponse(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/AdServicesStatusUtils.java b/android-34/android/adservices/common/AdServicesStatusUtils.java
new file mode 100644
index 0000000..97e32d1
--- /dev/null
+++ b/android-34/android/adservices/common/AdServicesStatusUtils.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.LimitExceededException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Utility class containing status codes and functions used by various response objects.
+ *
+ * <p>Those status codes are internal only.
+ *
+ * @hide
+ */
+public class AdServicesStatusUtils {
+ /**
+ * The status code has not been set. Keep unset status code the lowest value of the status
+ * codes.
+ */
+ public static final int STATUS_UNSET = -1;
+ /** The call was successful. */
+ public static final int STATUS_SUCCESS = 0;
+ /**
+ * An internal error occurred within the API, which the caller cannot address.
+ *
+ * <p>This error may be considered similar to {@link IllegalStateException}.
+ */
+ public static final int STATUS_INTERNAL_ERROR = 1;
+ /**
+ * The caller supplied invalid arguments to the call.
+ *
+ * <p>This error may be considered similar to {@link IllegalArgumentException}.
+ */
+ public static final int STATUS_INVALID_ARGUMENT = 2;
+ /** There was an unknown error. */
+ public static final int STATUS_UNKNOWN_ERROR = 3;
+ /**
+ * There was an I/O error.
+ *
+ * <p>This error may be considered similar to {@link IOException}.
+ */
+ public static final int STATUS_IO_ERROR = 4;
+ /**
+ * Result code for Rate Limit Reached.
+ *
+ * <p>This error may be considered similar to {@link LimitExceededException}.
+ */
+ public static final int STATUS_RATE_LIMIT_REACHED = 5;
+ /**
+ * Killswitch was enabled. AdServices is not available.
+ *
+ * <p>This error may be considered similar to {@link IllegalStateException}.
+ */
+ public static final int STATUS_KILLSWITCH_ENABLED = 6;
+ /**
+ * User consent was revoked. AdServices is not available.
+ *
+ * <p>This error may be considered similar to {@link IllegalStateException}.
+ */
+ public static final int STATUS_USER_CONSENT_REVOKED = 7;
+ /**
+ * AdServices were disabled. AdServices is not available.
+ *
+ * <p>This error may be considered similar to {@link IllegalStateException}.
+ */
+ public static final int STATUS_ADSERVICES_DISABLED = 8;
+ /**
+ * The caller is not authorized to make this call. Permission was not requested.
+ *
+ * <p>This error may be considered similar to {@link SecurityException}.
+ */
+ public static final int STATUS_PERMISSION_NOT_REQUESTED = 9;
+ /**
+ * The caller is not authorized to make this call. Caller is not allowed (not present in the
+ * allowed list).
+ *
+ * <p>This error may be considered similar to {@link SecurityException}.
+ */
+ public static final int STATUS_CALLER_NOT_ALLOWED = 10;
+ /**
+ * The caller is not authorized to make this call. Call was executed from background thread.
+ *
+ * <p>This error may be considered similar to {@link IllegalStateException}.
+ */
+ public static final int STATUS_BACKGROUND_CALLER = 11;
+ /**
+ * The caller is not authorized to make this call.
+ *
+ * <p>This error may be considered similar to {@link SecurityException}.
+ */
+ public static final int STATUS_UNAUTHORIZED = 12;
+ /**
+ * There was an internal Timeout within the API, which is non-recoverable by the caller
+ *
+ * <p>This error may be considered similar to {@link java.util.concurrent.TimeoutException}
+ */
+ public static final int STATUS_TIMEOUT = 13;
+ /**
+ * The device is not running a version of WebView that supports JSSandbox, required for FLEDGE
+ * Ad Selection.
+ *
+ * <p>This error may be considered similar to {@link IllegalStateException}.
+ */
+ public static final int STATUS_JS_SANDBOX_UNAVAILABLE = 14;
+
+ /** The error message to be returned along with {@link IllegalStateException}. */
+ public static final String ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE = "Service is not available.";
+ /** The error message to be returned along with {@link LimitExceededException}. */
+ public static final String RATE_LIMIT_REACHED_ERROR_MESSAGE = "API rate limit exceeded.";
+ /**
+ * The error message to be returned along with {@link SecurityException} when permission was not
+ * requested in the manifest.
+ */
+ public static final String SECURITY_EXCEPTION_PERMISSION_NOT_REQUESTED_ERROR_MESSAGE =
+ "Caller is not authorized to call this API. Permission was not requested.";
+ /**
+ * The error message to be returned along with {@link SecurityException} when caller is not
+ * allowed to call AdServices (not present in the allowed list).
+ */
+ public static final String SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ERROR_MESSAGE =
+ "Caller is not authorized to call this API. Caller is not allowed.";
+ /**
+ * The error message to be returned along with {@link SecurityException} when call was executed
+ * from the background thread.
+ */
+ public static final String ILLEGAL_STATE_BACKGROUND_CALLER_ERROR_MESSAGE =
+ "Background thread is not allowed to call this service.";
+
+ /**
+ * The error message to be returned along with {@link SecurityException} when caller not allowed
+ * to perform this operation on behalf of the given package.
+ */
+ public static final String SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ON_BEHALF_ERROR_MESSAGE =
+ "Caller is not allowed to perform this operation on behalf of the given package.";
+
+ /** The error message to be returned along with {@link TimeoutException}. */
+ public static final String TIMED_OUT_ERROR_MESSAGE = "API timed out.";
+
+ /** Returns true for a successful status. */
+ public static boolean isSuccess(@StatusCode int statusCode) {
+ return statusCode == STATUS_SUCCESS;
+ }
+
+ /** Converts the input {@code statusCode} to an exception to be used in the callback. */
+ @NonNull
+ public static Exception asException(@StatusCode int statusCode) {
+ switch (statusCode) {
+ case STATUS_INVALID_ARGUMENT:
+ return new IllegalArgumentException();
+ case STATUS_IO_ERROR:
+ return new IOException();
+ case STATUS_KILLSWITCH_ENABLED: // Intentional fallthrough
+ case STATUS_USER_CONSENT_REVOKED: // Intentional fallthrough
+ case STATUS_JS_SANDBOX_UNAVAILABLE:
+ return new IllegalStateException(ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE);
+ case STATUS_PERMISSION_NOT_REQUESTED:
+ return new SecurityException(
+ SECURITY_EXCEPTION_PERMISSION_NOT_REQUESTED_ERROR_MESSAGE);
+ case STATUS_CALLER_NOT_ALLOWED:
+ return new SecurityException(SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ERROR_MESSAGE);
+ case STATUS_BACKGROUND_CALLER:
+ return new IllegalStateException(ILLEGAL_STATE_BACKGROUND_CALLER_ERROR_MESSAGE);
+ case STATUS_UNAUTHORIZED:
+ return new SecurityException(
+ SECURITY_EXCEPTION_CALLER_NOT_ALLOWED_ON_BEHALF_ERROR_MESSAGE);
+ case STATUS_TIMEOUT:
+ return new TimeoutException(TIMED_OUT_ERROR_MESSAGE);
+ case STATUS_RATE_LIMIT_REACHED:
+ return new LimitExceededException(RATE_LIMIT_REACHED_ERROR_MESSAGE);
+ default:
+ return new IllegalStateException();
+ }
+ }
+
+ /** Converts the {@link AdServicesResponse} to an exception to be used in the callback. */
+ @NonNull
+ public static Exception asException(@NonNull AdServicesResponse adServicesResponse) {
+ return asException(adServicesResponse.getStatusCode());
+ }
+
+ /**
+ * Result codes that are common across various APIs.
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = {"STATUS_"},
+ value = {
+ STATUS_UNSET,
+ STATUS_SUCCESS,
+ STATUS_INTERNAL_ERROR,
+ STATUS_INVALID_ARGUMENT,
+ STATUS_RATE_LIMIT_REACHED,
+ STATUS_UNKNOWN_ERROR,
+ STATUS_IO_ERROR,
+ STATUS_KILLSWITCH_ENABLED,
+ STATUS_USER_CONSENT_REVOKED,
+ STATUS_ADSERVICES_DISABLED,
+ STATUS_PERMISSION_NOT_REQUESTED,
+ STATUS_CALLER_NOT_ALLOWED,
+ STATUS_BACKGROUND_CALLER,
+ STATUS_UNAUTHORIZED,
+ STATUS_TIMEOUT,
+ STATUS_JS_SANDBOX_UNAVAILABLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StatusCode {}
+}
diff --git a/android-34/android/adservices/common/AdTechIdentifier.java b/android-34/android/adservices/common/AdTechIdentifier.java
new file mode 100644
index 0000000..e7fe66c
--- /dev/null
+++ b/android-34/android/adservices/common/AdTechIdentifier.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/** An Identifier representing an ad buyer or seller. */
+public final class AdTechIdentifier implements Parcelable {
+
+ @NonNull private final String mIdentifier;
+
+ private AdTechIdentifier(@NonNull Parcel in) {
+ this(in.readString());
+ }
+
+ private AdTechIdentifier(@NonNull String adTechIdentifier) {
+ this(adTechIdentifier, true);
+ }
+
+ private AdTechIdentifier(@NonNull String adTechIdentifier, boolean validate) {
+ Objects.requireNonNull(adTechIdentifier);
+ if (validate) {
+ validate(adTechIdentifier);
+ }
+ mIdentifier = adTechIdentifier;
+ }
+
+ @NonNull
+ public static final Creator<AdTechIdentifier> CREATOR =
+ new Creator<AdTechIdentifier>() {
+ @Override
+ public AdTechIdentifier createFromParcel(Parcel in) {
+ Objects.requireNonNull(in);
+ return new AdTechIdentifier(in);
+ }
+
+ @Override
+ public AdTechIdentifier[] newArray(int size) {
+ return new AdTechIdentifier[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ dest.writeString(mIdentifier);
+ }
+
+ /**
+ * Compares this AdTechIdentifier to the specified object. The result is true if and only if the
+ * argument is not null and is a AdTechIdentifier object with the same string form (obtained by
+ * calling {@link #toString()}). Note that this method will not perform any eTLD+1 normalization
+ * so two AdTechIdentifier objects with the same eTLD+1 could be not equal if the String
+ * representations of the objects was not equal.
+ *
+ * @param o The object to compare this AdTechIdentifier against
+ * @return true if the given object represents an AdTechIdentifier equivalent to this
+ * AdTechIdentifier, false otherwise
+ */
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof AdTechIdentifier
+ && mIdentifier.equals(((AdTechIdentifier) o).toString());
+ }
+
+ /**
+ * Returns a hash code corresponding to the string representation of this class obtained by
+ * calling {@link #toString()}. Note that this method will not perform any eTLD+1 normalization
+ * so two AdTechIdentifier objects with the same eTLD+1 could have different hash codes if the
+ * underlying string representation was different.
+ *
+ * @return a hash code value for this object.
+ */
+ @Override
+ public int hashCode() {
+ return mIdentifier.hashCode();
+ }
+
+ /** @return The identifier in String form. */
+ @Override
+ @NonNull
+ public String toString() {
+ return mIdentifier;
+ }
+
+ /**
+ * Construct an instance of this class from a String.
+ *
+ * @param source A valid eTLD+1 domain of an ad buyer or seller or null.
+ * @return An {@link AdTechIdentifier} class wrapping the given domain or null if the input was
+ * null.
+ */
+ @NonNull
+ public static AdTechIdentifier fromString(@NonNull String source) {
+ return AdTechIdentifier.fromString(source, true);
+ }
+
+ /**
+ * Construct an instance of this class from a String.
+ *
+ * @param source A valid eTLD+1 domain of an ad buyer or seller.
+ * @param validate Construction-time validation is run on the string if and only if this is
+ * true.
+ * @return An {@link AdTechIdentifier} class wrapping the given domain.
+ * @hide
+ */
+ @NonNull
+ public static AdTechIdentifier fromString(@NonNull String source, boolean validate) {
+ return new AdTechIdentifier(source, validate);
+ }
+
+ private void validate(String inputString) {
+ // TODO(b/238849930) Bring existing validation function here
+ }
+}
diff --git a/android-34/android/adservices/common/AppInstallFilters.java b/android-34/android/adservices/common/AppInstallFilters.java
new file mode 100644
index 0000000..20aebaf
--- /dev/null
+++ b/android-34/android/adservices/common/AppInstallFilters.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+// TODO(b/266837113) link to setAppInstallAdvertisers once unhidden.
+
+/**
+ * A container for the ad filters that are based on app install state.
+ *
+ * <p>App install filters filter out ads based on the presence of packages installed on the device.
+ * In order for filtering to work, a package must call the setAppInstallAdvertisers API with the
+ * identifier of the adtech who owns this ad. If that call has been made, and the ad contains an
+ * {@link AppInstallFilters} object whose package name set contains the name of the package, the ad
+ * will be removed from the auction.
+ *
+ * <p>Note that the filtering is based on any package with one of the listed package names being on
+ * the device. It is possible that the package holding the package name is not the application
+ * targeted by the ad.
+ *
+ * @hide
+ */
+public final class AppInstallFilters implements Parcelable {
+ /** @hide */
+ @VisibleForTesting public static final String PACKAGE_NAMES_FIELD_NAME = "package_names";
+
+ @NonNull private final Set<String> mPackageNames;
+
+ @NonNull
+ public static final Creator<AppInstallFilters> CREATOR =
+ new Creator<AppInstallFilters>() {
+ @NonNull
+ @Override
+ public AppInstallFilters createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new AppInstallFilters(in);
+ }
+
+ @NonNull
+ @Override
+ public AppInstallFilters[] newArray(int size) {
+ return new AppInstallFilters[size];
+ }
+ };
+
+ private AppInstallFilters(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mPackageNames = builder.mPackageNames;
+ }
+
+ private AppInstallFilters(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mPackageNames = AdServicesParcelableUtil.readStringSetFromParcel(in);
+ }
+
+ /**
+ * Gets the list of package names this ad is filtered on.
+ *
+ * <p>The ad containing this filter will be removed from the ad auction if any of the package
+ * names are present on the device and have called setAppInstallAdvertisers.
+ */
+ @NonNull
+ public Set<String> getPackageNames() {
+ return mPackageNames;
+ }
+
+ /**
+ * @return The estimated size of this object, in bytes.
+ * @hide
+ */
+ public int getSizeInBytes() {
+ int totalSize = 0;
+ for (String packageName : mPackageNames) {
+ totalSize += packageName.getBytes().length;
+ }
+ return totalSize;
+ }
+
+ /**
+ * A JSON serializer.
+ *
+ * @return A JSON serialization of this object.
+ * @hide
+ */
+ public JSONObject toJson() throws JSONException {
+ JSONObject toReturn = new JSONObject();
+ JSONArray packageNames = new JSONArray();
+ for (String packageName : mPackageNames) {
+ packageNames.put(packageName);
+ }
+ toReturn.put(PACKAGE_NAMES_FIELD_NAME, packageNames);
+ return toReturn;
+ }
+
+ /**
+ * A JSON de-serializer.
+ *
+ * @param json A JSON representation of an {@link AppInstallFilters} object as would be
+ * generated by {@link #toJson()}.
+ * @return An {@link AppInstallFilters} object generated from the given JSON.
+ * @hide
+ */
+ public static AppInstallFilters fromJson(JSONObject json) throws JSONException {
+ JSONArray serializedPackageNames = json.getJSONArray(PACKAGE_NAMES_FIELD_NAME);
+ Set<String> packageNames = new HashSet<>();
+ for (int i = 0; i < serializedPackageNames.length(); i++) {
+ Object packageName = serializedPackageNames.get(i);
+ if (packageName instanceof String) {
+ packageNames.add((String) packageName);
+ } else {
+ throw new JSONException(
+ "Found non-string package name when de-serializing AppInstallFilters");
+ }
+ }
+ return new Builder().setPackageNames(packageNames).build();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ AdServicesParcelableUtil.writeStringSetToParcel(dest, mPackageNames);
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Checks whether the {@link AppInstallFilters} objects contain the same information. */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AppInstallFilters)) return false;
+ AppInstallFilters that = (AppInstallFilters) o;
+ return mPackageNames.equals(that.mPackageNames);
+ }
+
+ /** Returns the hash of the {@link AppInstallFilters} object's data. */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageNames);
+ }
+
+ @Override
+ public String toString() {
+ return "AppInstallFilters{" + "mPackageNames=" + mPackageNames + '}';
+ }
+
+ /** Builder for creating {@link AppInstallFilters} objects. */
+ public static final class Builder {
+ @NonNull private Set<String> mPackageNames = new HashSet<>();
+
+ public Builder() {}
+
+ /**
+ * Gets the list of package names this ad is filtered on.
+ *
+ * <p>See {@link #getPackageNames()} for more information.
+ */
+ @NonNull
+ public Builder setPackageNames(@NonNull Set<String> packageNames) {
+ Objects.requireNonNull(packageNames);
+ mPackageNames = packageNames;
+ return this;
+ }
+
+ /** Builds and returns a {@link AppInstallFilters} instance. */
+ @NonNull
+ public AppInstallFilters build() {
+ return new AppInstallFilters(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/CallerMetadata.java b/android-34/android/adservices/common/CallerMetadata.java
new file mode 100644
index 0000000..4c3be11
--- /dev/null
+++ b/android-34/android/adservices/common/CallerMetadata.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A class to hold the metadata of an IPC call.
+ *
+ * @hide
+ */
+public class CallerMetadata implements Parcelable {
+ private @NonNull long mBinderElapsedTimestamp;
+
+ private CallerMetadata(@NonNull long binderElapsedTimestamp) {
+ mBinderElapsedTimestamp = binderElapsedTimestamp;
+ }
+
+ private CallerMetadata(@NonNull Parcel in) {
+ mBinderElapsedTimestamp = in.readLong();
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<CallerMetadata> CREATOR =
+ new Parcelable.Creator<CallerMetadata>() {
+ @Override
+ public CallerMetadata createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new CallerMetadata(in);
+ }
+
+ @Override
+ public CallerMetadata[] newArray(int size) {
+ return new CallerMetadata[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeLong(mBinderElapsedTimestamp);
+ }
+
+ /** Get the binder elapsed timestamp. */
+ public long getBinderElapsedTimestamp() {
+ return mBinderElapsedTimestamp;
+ }
+
+ /** Builder for {@link CallerMetadata} objects. */
+ public static final class Builder {
+ private long mBinderElapsedTimestamp;
+
+ public Builder() {
+ }
+
+ /** Set the binder elapsed timestamp. */
+ public @NonNull CallerMetadata.Builder setBinderElapsedTimestamp(
+ @NonNull long binderElapsedTimestamp) {
+ mBinderElapsedTimestamp = binderElapsedTimestamp;
+ return this;
+ }
+
+ /** Builds a {@link CallerMetadata} instance. */
+ public @NonNull CallerMetadata build() {
+ return new CallerMetadata(mBinderElapsedTimestamp);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/FledgeErrorResponse.java b/android-34/android/adservices/common/FledgeErrorResponse.java
new file mode 100644
index 0000000..10274d6
--- /dev/null
+++ b/android-34/android/adservices/common/FledgeErrorResponse.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.adservices.common.AdServicesStatusUtils.StatusCode;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Represent a generic response for FLEDGE API's.
+ *
+ * @hide
+ */
+public final class FledgeErrorResponse extends AdServicesResponse {
+
+ private FledgeErrorResponse(@StatusCode int statusCode, @Nullable String errorMessage) {
+ super(statusCode, errorMessage);
+ }
+
+ private FledgeErrorResponse(@NonNull Parcel in) {
+ super(in);
+ }
+
+ @NonNull
+ public static final Creator<FledgeErrorResponse> CREATOR =
+ new Parcelable.Creator<FledgeErrorResponse>() {
+ @Override
+ public FledgeErrorResponse createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new FledgeErrorResponse(in);
+ }
+
+ @Override
+ public FledgeErrorResponse[] newArray(int size) {
+ return new FledgeErrorResponse[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ dest.writeInt(mStatusCode);
+ dest.writeString(mErrorMessage);
+ }
+
+ @Override
+ public String toString() {
+ return "FledgeErrorResponse{"
+ + "mStatusCode="
+ + mStatusCode
+ + ", mErrorMessage='"
+ + mErrorMessage
+ + "'}";
+ }
+
+ /**
+ * Builder for {@link FledgeErrorResponse} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @StatusCode private int mStatusCode = AdServicesStatusUtils.STATUS_UNSET;
+ @Nullable private String mErrorMessage;
+
+ public Builder() {}
+
+ /** Set the Status Code. */
+ @NonNull
+ public FledgeErrorResponse.Builder setStatusCode(@StatusCode int statusCode) {
+ mStatusCode = statusCode;
+ return this;
+ }
+
+ /** Set the Error Message. */
+ @NonNull
+ public FledgeErrorResponse.Builder setErrorMessage(@Nullable String errorMessage) {
+ mErrorMessage = errorMessage;
+ return this;
+ }
+
+ /**
+ * Builds a {@link FledgeErrorResponse} instance.
+ *
+ * <p>throws IllegalArgumentException if any of the status code is null or error message is
+ * not set for an unsuccessful status
+ */
+ @NonNull
+ public FledgeErrorResponse build() {
+ Preconditions.checkArgument(
+ mStatusCode != AdServicesStatusUtils.STATUS_UNSET,
+ "Status code has not been set!");
+
+ return new FledgeErrorResponse(mStatusCode, mErrorMessage);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/FrequencyCapFilters.java b/android-34/android/adservices/common/FrequencyCapFilters.java
new file mode 100644
index 0000000..cb70886
--- /dev/null
+++ b/android-34/android/adservices/common/FrequencyCapFilters.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.adservices.adselection.ReportImpressionRequest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.OutcomeReceiver;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * A container for the ad filters that are based on frequency caps.
+ *
+ * <p>Frequency caps filters combine an event type with a set of {@link KeyedFrequencyCap} objects
+ * to define a set of ad filters. If any of these frequency caps are exceeded for a given ad, the ad
+ * will be removed from the group of ads submitted to a buyer adtech's bidding function.
+ *
+ * @hide
+ */
+// TODO(b/221876775): Unhide for frequency cap API review
+public final class FrequencyCapFilters implements Parcelable {
+ /**
+ * Event types which are used to update ad counter histograms, which inform frequency cap
+ * filtering in FLEDGE.
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = {"AD_EVENT_TYPE_"},
+ value = {
+ AD_EVENT_TYPE_INVALID,
+ AD_EVENT_TYPE_WIN,
+ AD_EVENT_TYPE_IMPRESSION,
+ AD_EVENT_TYPE_VIEW,
+ AD_EVENT_TYPE_CLICK
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AdEventType {}
+
+ /** @hide */
+ public static final int AD_EVENT_TYPE_INVALID = -1;
+
+ /**
+ * The WIN ad event type is automatically populated within the FLEDGE service for any winning ad
+ * which is returned from FLEDGE ad selection.
+ *
+ * <p>It should not be used to manually update an ad counter histogram.
+ */
+ public static final int AD_EVENT_TYPE_WIN = 0;
+
+ public static final int AD_EVENT_TYPE_IMPRESSION = 1;
+ public static final int AD_EVENT_TYPE_VIEW = 2;
+ public static final int AD_EVENT_TYPE_CLICK = 3;
+ /** @hide */
+ @VisibleForTesting public static final String WIN_EVENTS_FIELD_NAME = "win";
+ /** @hide */
+ @VisibleForTesting public static final String IMPRESSION_EVENTS_FIELD_NAME = "impression";
+ /** @hide */
+ @VisibleForTesting public static final String VIEW_EVENTS_FIELD_NAME = "view";
+ /** @hide */
+ @VisibleForTesting public static final String CLICK_EVENTS_FIELD_NAME = "click";
+
+ @NonNull private final Set<KeyedFrequencyCap> mKeyedFrequencyCapsForWinEvents;
+ @NonNull private final Set<KeyedFrequencyCap> mKeyedFrequencyCapsForImpressionEvents;
+ @NonNull private final Set<KeyedFrequencyCap> mKeyedFrequencyCapsForViewEvents;
+ @NonNull private final Set<KeyedFrequencyCap> mKeyedFrequencyCapsForClickEvents;
+
+ @NonNull
+ public static final Creator<FrequencyCapFilters> CREATOR =
+ new Creator<FrequencyCapFilters>() {
+ @Override
+ public FrequencyCapFilters createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new FrequencyCapFilters(in);
+ }
+
+ @Override
+ public FrequencyCapFilters[] newArray(int size) {
+ return new FrequencyCapFilters[size];
+ }
+ };
+
+ private FrequencyCapFilters(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mKeyedFrequencyCapsForWinEvents = builder.mKeyedFrequencyCapsForWinEvents;
+ mKeyedFrequencyCapsForImpressionEvents = builder.mKeyedFrequencyCapsForImpressionEvents;
+ mKeyedFrequencyCapsForViewEvents = builder.mKeyedFrequencyCapsForViewEvents;
+ mKeyedFrequencyCapsForClickEvents = builder.mKeyedFrequencyCapsForClickEvents;
+ }
+
+ private FrequencyCapFilters(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mKeyedFrequencyCapsForWinEvents =
+ AdServicesParcelableUtil.readSetFromParcel(in, KeyedFrequencyCap.CREATOR);
+ mKeyedFrequencyCapsForImpressionEvents =
+ AdServicesParcelableUtil.readSetFromParcel(in, KeyedFrequencyCap.CREATOR);
+ mKeyedFrequencyCapsForViewEvents =
+ AdServicesParcelableUtil.readSetFromParcel(in, KeyedFrequencyCap.CREATOR);
+ mKeyedFrequencyCapsForClickEvents =
+ AdServicesParcelableUtil.readSetFromParcel(in, KeyedFrequencyCap.CREATOR);
+ }
+
+ /**
+ * Gets the set of {@link KeyedFrequencyCap} objects that will filter on the {@link
+ * #AD_EVENT_TYPE_WIN} event type.
+ *
+ * <p>These frequency caps apply to events for ads that were selected as winners in ad
+ * selection. Winning ads are used to automatically increment the associated counter keys on the
+ * win event type.
+ */
+ @NonNull
+ public Set<KeyedFrequencyCap> getKeyedFrequencyCapsForWinEvents() {
+ return mKeyedFrequencyCapsForWinEvents;
+ }
+
+ /**
+ * Gets the set of {@link KeyedFrequencyCap} objects that will filter on the {@link
+ * #AD_EVENT_TYPE_IMPRESSION} event type.
+ *
+ * <p>These frequency caps apply to events which correlate to an impression as interpreted by an
+ * adtech. Note that events are not automatically counted when calling {@link
+ * android.adservices.adselection.AdSelectionManager#reportImpression(ReportImpressionRequest,
+ * Executor, OutcomeReceiver)}.
+ */
+ @NonNull
+ public Set<KeyedFrequencyCap> getKeyedFrequencyCapsForImpressionEvents() {
+ return mKeyedFrequencyCapsForImpressionEvents;
+ }
+
+ /**
+ * Gets the set of {@link KeyedFrequencyCap} objects that will filter on the {@link
+ * #AD_EVENT_TYPE_VIEW} event type.
+ *
+ * <p>These frequency caps apply to events which correlate to a view as interpreted by an
+ * adtech.
+ */
+ @NonNull
+ public Set<KeyedFrequencyCap> getKeyedFrequencyCapsForViewEvents() {
+ return mKeyedFrequencyCapsForViewEvents;
+ }
+
+ /**
+ * Gets the set of {@link KeyedFrequencyCap} objects that will filter on the {@link
+ * #AD_EVENT_TYPE_CLICK} event type.
+ *
+ * <p>These frequency caps apply to events which correlate to a click as interpreted by an
+ * adtech.
+ */
+ @NonNull
+ public Set<KeyedFrequencyCap> getKeyedFrequencyCapsForClickEvents() {
+ return mKeyedFrequencyCapsForClickEvents;
+ }
+
+ /**
+ * @return The estimated size of this object, in bytes.
+ * @hide
+ */
+ public int getSizeInBytes() {
+ return getSizeInBytesOfFcapSet(mKeyedFrequencyCapsForWinEvents)
+ + getSizeInBytesOfFcapSet(mKeyedFrequencyCapsForImpressionEvents)
+ + getSizeInBytesOfFcapSet(mKeyedFrequencyCapsForViewEvents)
+ + getSizeInBytesOfFcapSet(mKeyedFrequencyCapsForClickEvents);
+ }
+
+ private int getSizeInBytesOfFcapSet(Set<KeyedFrequencyCap> fcaps) {
+ int toReturn = 0;
+ for (final KeyedFrequencyCap fcap : fcaps) {
+ toReturn += fcap.getSizeInBytes();
+ }
+ return toReturn;
+ }
+
+ /**
+ * A JSON serializer.
+ *
+ * @return A JSON serialization of this object.
+ * @hide
+ */
+ public JSONObject toJson() throws JSONException {
+ JSONObject toReturn = new JSONObject();
+ toReturn.put(WIN_EVENTS_FIELD_NAME, fcapSetToJsonArray(mKeyedFrequencyCapsForWinEvents));
+ toReturn.put(
+ IMPRESSION_EVENTS_FIELD_NAME,
+ fcapSetToJsonArray(mKeyedFrequencyCapsForImpressionEvents));
+ toReturn.put(VIEW_EVENTS_FIELD_NAME, fcapSetToJsonArray(mKeyedFrequencyCapsForViewEvents));
+ toReturn.put(
+ CLICK_EVENTS_FIELD_NAME, fcapSetToJsonArray(mKeyedFrequencyCapsForClickEvents));
+ return toReturn;
+ }
+
+ private static JSONArray fcapSetToJsonArray(Set<KeyedFrequencyCap> fcapSet)
+ throws JSONException {
+ JSONArray toReturn = new JSONArray();
+ for (KeyedFrequencyCap fcap : fcapSet) {
+ toReturn.put(fcap.toJson());
+ }
+ return toReturn;
+ }
+
+ /**
+ * A JSON de-serializer.
+ *
+ * @param json A JSON representation of an {@link FrequencyCapFilters} object as would be
+ * generated by {@link #toJson()}.
+ * @return An {@link FrequencyCapFilters} object generated from the given JSON.
+ * @hide
+ */
+ public static FrequencyCapFilters fromJson(JSONObject json) throws JSONException {
+ Builder builder = new Builder();
+ if (json.has(WIN_EVENTS_FIELD_NAME)) {
+ builder.setKeyedFrequencyCapsForWinEvents(
+ jsonArrayToFcapSet(json.getJSONArray(WIN_EVENTS_FIELD_NAME)));
+ }
+ if (json.has(IMPRESSION_EVENTS_FIELD_NAME)) {
+ builder.setKeyedFrequencyCapsForImpressionEvents(
+ jsonArrayToFcapSet(json.getJSONArray(IMPRESSION_EVENTS_FIELD_NAME)));
+ }
+ if (json.has(VIEW_EVENTS_FIELD_NAME)) {
+ builder.setKeyedFrequencyCapsForViewEvents(
+ jsonArrayToFcapSet(json.getJSONArray(VIEW_EVENTS_FIELD_NAME)));
+ }
+ if (json.has(CLICK_EVENTS_FIELD_NAME)) {
+ builder.setKeyedFrequencyCapsForClickEvents(
+ jsonArrayToFcapSet(json.getJSONArray(CLICK_EVENTS_FIELD_NAME)));
+ }
+ return builder.build();
+ }
+
+ private static Set<KeyedFrequencyCap> jsonArrayToFcapSet(JSONArray json) throws JSONException {
+ Set<KeyedFrequencyCap> toReturn = new HashSet<>();
+ for (int i = 0; i < json.length(); i++) {
+ toReturn.add(KeyedFrequencyCap.fromJson(json.getJSONObject(i)));
+ }
+ return toReturn;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ AdServicesParcelableUtil.writeSetToParcel(dest, mKeyedFrequencyCapsForWinEvents);
+ AdServicesParcelableUtil.writeSetToParcel(dest, mKeyedFrequencyCapsForImpressionEvents);
+ AdServicesParcelableUtil.writeSetToParcel(dest, mKeyedFrequencyCapsForViewEvents);
+ AdServicesParcelableUtil.writeSetToParcel(dest, mKeyedFrequencyCapsForClickEvents);
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Checks whether the {@link FrequencyCapFilters} objects contain the same information. */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof FrequencyCapFilters)) return false;
+ FrequencyCapFilters that = (FrequencyCapFilters) o;
+ return mKeyedFrequencyCapsForWinEvents.equals(that.mKeyedFrequencyCapsForWinEvents)
+ && mKeyedFrequencyCapsForImpressionEvents.equals(
+ that.mKeyedFrequencyCapsForImpressionEvents)
+ && mKeyedFrequencyCapsForViewEvents.equals(that.mKeyedFrequencyCapsForViewEvents)
+ && mKeyedFrequencyCapsForClickEvents.equals(that.mKeyedFrequencyCapsForClickEvents);
+ }
+
+ /** Returns the hash of the {@link FrequencyCapFilters} object's data. */
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mKeyedFrequencyCapsForWinEvents,
+ mKeyedFrequencyCapsForImpressionEvents,
+ mKeyedFrequencyCapsForViewEvents,
+ mKeyedFrequencyCapsForClickEvents);
+ }
+
+ @Override
+ public String toString() {
+ return "FrequencyCapFilters{"
+ + "mKeyedFrequencyCapsForWinEvents="
+ + mKeyedFrequencyCapsForWinEvents
+ + ", mKeyedFrequencyCapsForImpressionEvents="
+ + mKeyedFrequencyCapsForImpressionEvents
+ + ", mKeyedFrequencyCapsForViewEvents="
+ + mKeyedFrequencyCapsForViewEvents
+ + ", mKeyedFrequencyCapsForClickEvents="
+ + mKeyedFrequencyCapsForClickEvents
+ + '}';
+ }
+
+ /** Builder for creating {@link FrequencyCapFilters} objects. */
+ public static final class Builder {
+ @NonNull private Set<KeyedFrequencyCap> mKeyedFrequencyCapsForWinEvents = new HashSet<>();
+
+ @NonNull
+ private Set<KeyedFrequencyCap> mKeyedFrequencyCapsForImpressionEvents = new HashSet<>();
+
+ @NonNull private Set<KeyedFrequencyCap> mKeyedFrequencyCapsForViewEvents = new HashSet<>();
+ @NonNull private Set<KeyedFrequencyCap> mKeyedFrequencyCapsForClickEvents = new HashSet<>();
+
+ public Builder() {}
+
+ /**
+ * Sets the set of {@link KeyedFrequencyCap} objects that will filter on the {@link
+ * #AD_EVENT_TYPE_WIN} event type.
+ *
+ * <p>See {@link #getKeyedFrequencyCapsForWinEvents()} for more information.
+ */
+ @NonNull
+ public Builder setKeyedFrequencyCapsForWinEvents(
+ @NonNull Set<KeyedFrequencyCap> keyedFrequencyCapsForWinEvents) {
+ Objects.requireNonNull(keyedFrequencyCapsForWinEvents);
+ mKeyedFrequencyCapsForWinEvents = keyedFrequencyCapsForWinEvents;
+ return this;
+ }
+
+ /**
+ * Sets the set of {@link KeyedFrequencyCap} objects that will filter on the {@link
+ * #AD_EVENT_TYPE_IMPRESSION} event type.
+ *
+ * <p>See {@link #getKeyedFrequencyCapsForImpressionEvents()} for more information.
+ */
+ @NonNull
+ public Builder setKeyedFrequencyCapsForImpressionEvents(
+ @NonNull Set<KeyedFrequencyCap> keyedFrequencyCapsForImpressionEvents) {
+ Objects.requireNonNull(keyedFrequencyCapsForImpressionEvents);
+ mKeyedFrequencyCapsForImpressionEvents = keyedFrequencyCapsForImpressionEvents;
+ return this;
+ }
+
+ /**
+ * Sets the set of {@link KeyedFrequencyCap} objects that will filter on the {@link
+ * #AD_EVENT_TYPE_VIEW} event type.
+ *
+ * <p>See {@link #getKeyedFrequencyCapsForViewEvents()} for more information.
+ */
+ @NonNull
+ public Builder setKeyedFrequencyCapsForViewEvents(
+ @NonNull Set<KeyedFrequencyCap> keyedFrequencyCapsForViewEvents) {
+ Objects.requireNonNull(keyedFrequencyCapsForViewEvents);
+ mKeyedFrequencyCapsForViewEvents = keyedFrequencyCapsForViewEvents;
+ return this;
+ }
+
+ /**
+ * Sets the set of {@link KeyedFrequencyCap} objects that will filter on the {@link
+ * #AD_EVENT_TYPE_CLICK} event type.
+ *
+ * <p>See {@link #getKeyedFrequencyCapsForClickEvents()} for more information.
+ */
+ @NonNull
+ public Builder setKeyedFrequencyCapsForClickEvents(
+ @NonNull Set<KeyedFrequencyCap> keyedFrequencyCapsForClickEvents) {
+ Objects.requireNonNull(keyedFrequencyCapsForClickEvents);
+ mKeyedFrequencyCapsForClickEvents = keyedFrequencyCapsForClickEvents;
+ return this;
+ }
+
+ /** Builds and returns a {@link FrequencyCapFilters} instance. */
+ @NonNull
+ public FrequencyCapFilters build() {
+ return new FrequencyCapFilters(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/IsAdServicesEnabledResult.java b/android-34/android/adservices/common/IsAdServicesEnabledResult.java
new file mode 100644
index 0000000..a9d8728
--- /dev/null
+++ b/android-34/android/adservices/common/IsAdServicesEnabledResult.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Result from the isAdServicesEnabled API.
+ *
+ * @hide
+ */
+public final class IsAdServicesEnabledResult implements Parcelable {
+ @Nullable private final String mErrorMessage;
+ private final boolean mAdServicesEnabled;
+
+ private IsAdServicesEnabledResult(@Nullable String errorMessage, @NonNull boolean enabled) {
+ mErrorMessage = errorMessage;
+ mAdServicesEnabled = enabled;
+ }
+
+ private IsAdServicesEnabledResult(@NonNull Parcel in) {
+ mErrorMessage = in.readString();
+ mAdServicesEnabled = in.readBoolean();
+ }
+
+ public static final @NonNull Creator<IsAdServicesEnabledResult> CREATOR =
+ new Creator<IsAdServicesEnabledResult>() {
+ @Override
+ public IsAdServicesEnabledResult createFromParcel(Parcel in) {
+ return new IsAdServicesEnabledResult(in);
+ }
+
+ @Override
+ public IsAdServicesEnabledResult[] newArray(int size) {
+ return new IsAdServicesEnabledResult[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeString(mErrorMessage);
+ out.writeBoolean(mAdServicesEnabled);
+ }
+
+ /** Returns the error message associated with this result. */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /** Returns the Adservices enabled status. */
+ @NonNull
+ public boolean getAdServicesEnabled() {
+ return mAdServicesEnabled;
+ }
+
+ @Override
+ public String toString() {
+ return "GetAdserviceStatusResult{"
+ + ", mErrorMessage='"
+ + mErrorMessage
+ + ", mAdservicesEnabled="
+ + mAdServicesEnabled
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof IsAdServicesEnabledResult)) {
+ return false;
+ }
+
+ IsAdServicesEnabledResult that = (IsAdServicesEnabledResult) o;
+
+ return Objects.equals(mErrorMessage, that.mErrorMessage)
+ && mAdServicesEnabled == that.mAdServicesEnabled;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mErrorMessage, mAdServicesEnabled);
+ }
+
+ /**
+ * Builder for {@link IsAdServicesEnabledResult} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @Nullable private String mErrorMessage;
+ private boolean mAdServicesEnabled;
+
+ public Builder() {}
+
+ /** Set the Error Message. */
+ public @NonNull Builder setErrorMessage(@Nullable String errorMessage) {
+ mErrorMessage = errorMessage;
+ return this;
+ }
+
+ /** Set the list of the returned Status */
+ public @NonNull Builder setAdServicesEnabled(@NonNull boolean adServicesEnabled) {
+ mAdServicesEnabled = adServicesEnabled;
+ return this;
+ }
+
+ /**
+ * Builds a {@link IsAdServicesEnabledResult} instance.
+ *
+ * <p>throws IllegalArgumentException if any of the params are null or there is any mismatch
+ * in the size of ModelVersions and TaxonomyVersions.
+ */
+ public @NonNull IsAdServicesEnabledResult build() {
+ return new IsAdServicesEnabledResult(mErrorMessage, mAdServicesEnabled);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/KeyedFrequencyCap.java b/android-34/android/adservices/common/KeyedFrequencyCap.java
new file mode 100644
index 0000000..d0e5c93
--- /dev/null
+++ b/android-34/android/adservices/common/KeyedFrequencyCap.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * A frequency cap for a specific ad counter key.
+ *
+ * <p>Frequency caps define the maximum count of previously counted events within a given time
+ * interval. If the frequency cap is exceeded, the associated ad will be filtered out of ad
+ * selection.
+ *
+ * @hide
+ */
+// TODO(b/221876775): Unhide for frequency cap API review
+public final class KeyedFrequencyCap implements Parcelable {
+ /** @hide */
+ @VisibleForTesting public static final String AD_COUNTER_KEY_FIELD_NAME = "ad_counter_key";
+ /** @hide */
+ @VisibleForTesting public static final String MAX_COUNT_FIELD_NAME = "max_count";
+ /** @hide */
+ @VisibleForTesting public static final String INTERVAL_FIELD_NAME = "interval_in_seconds";
+ /** @hide */
+ @VisibleForTesting public static final String JSON_ERROR_POSTFIX = " must be a String.";
+ // 12 bytes for the duration and 4 for the maxCount
+ private static final int SIZE_OF_FIXED_FIELDS = 16;
+ @NonNull private final String mAdCounterKey;
+ private final int mMaxCount;
+ @NonNull private final Duration mInterval;
+
+ @NonNull
+ public static final Creator<KeyedFrequencyCap> CREATOR =
+ new Creator<KeyedFrequencyCap>() {
+ @Override
+ public KeyedFrequencyCap createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new KeyedFrequencyCap(in);
+ }
+
+ @Override
+ public KeyedFrequencyCap[] newArray(int size) {
+ return new KeyedFrequencyCap[size];
+ }
+ };
+
+ private KeyedFrequencyCap(@NonNull Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mAdCounterKey = builder.mAdCounterKey;
+ mMaxCount = builder.mMaxCount;
+ mInterval = builder.mInterval;
+ }
+
+ private KeyedFrequencyCap(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mAdCounterKey = in.readString();
+ mMaxCount = in.readInt();
+ mInterval = Duration.ofSeconds(in.readLong());
+ }
+
+ /**
+ * Returns the ad counter key that the frequency cap is applied to.
+ *
+ * <p>The ad counter key is defined by an adtech and is an arbitrary string which defines any
+ * criteria which may have previously been counted and persisted on the device. If the on-device
+ * count exceeds the maximum count within a certain time interval, the frequency cap has been
+ * exceeded.
+ */
+ @NonNull
+ public String getAdCounterKey() {
+ return mAdCounterKey;
+ }
+
+ /**
+ * Returns the maximum count of previously occurring events allowed within a given time
+ * interval.
+ *
+ * <p>If there are more events matching the ad counter key and ad event type counted on the
+ * device within the time interval defined by {@link #getInterval()}, the frequency cap has been
+ * exceeded, and the ad will not be eligible for ad selection.
+ *
+ * <p>For example, an ad that specifies a filter for a max count of two within one hour will not
+ * be eligible for ad selection if the event has been counted three or more times within the
+ * hour preceding the ad selection process.
+ */
+ public int getMaxCount() {
+ return mMaxCount;
+ }
+
+ /**
+ * Returns the interval, as a {@link Duration} which will be truncated to the nearest second,
+ * over which the frequency cap is calculated.
+ *
+ * <p>When this frequency cap is computed, the number of persisted events is counted in the most
+ * recent time interval. If the count of previously occurring matching events for an adtech is
+ * greater than the number returned by {@link #getMaxCount()}, the frequency cap has been
+ * exceeded, and the ad will not be eligible for ad selection.
+ */
+ @NonNull
+ public Duration getInterval() {
+ return mInterval;
+ }
+
+ /**
+ * @return The estimated size of this object, in bytes.
+ * @hide
+ */
+ public int getSizeInBytes() {
+ return mAdCounterKey.getBytes().length + SIZE_OF_FIXED_FIELDS;
+ }
+
+ /**
+ * A JSON serializer.
+ *
+ * @return A JSON serialization of this object.
+ * @hide
+ */
+ public JSONObject toJson() throws JSONException {
+ JSONObject toReturn = new JSONObject();
+ toReturn.put(AD_COUNTER_KEY_FIELD_NAME, mAdCounterKey);
+ toReturn.put(MAX_COUNT_FIELD_NAME, mMaxCount);
+ toReturn.put(INTERVAL_FIELD_NAME, mInterval.getSeconds());
+ return toReturn;
+ }
+
+ /**
+ * A JSON de-serializer.
+ *
+ * @param json A JSON representation of an {@link KeyedFrequencyCap} object as would be
+ * generated by {@link #toJson()}.
+ * @return An {@link KeyedFrequencyCap} object generated from the given JSON.
+ * @hide
+ */
+ public static KeyedFrequencyCap fromJson(JSONObject json) throws JSONException {
+ Object adCounterKey = json.get(AD_COUNTER_KEY_FIELD_NAME);
+ if (!(adCounterKey instanceof String)) {
+ throw new JSONException(AD_COUNTER_KEY_FIELD_NAME + JSON_ERROR_POSTFIX);
+ }
+ return new Builder()
+ .setAdCounterKey((String) adCounterKey)
+ .setMaxCount(json.getInt(MAX_COUNT_FIELD_NAME))
+ .setInterval(Duration.ofSeconds(json.getLong(INTERVAL_FIELD_NAME)))
+ .build();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ dest.writeString(mAdCounterKey);
+ dest.writeInt(mMaxCount);
+ dest.writeLong(mInterval.getSeconds());
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Checks whether the {@link KeyedFrequencyCap} objects contain the same information. */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof KeyedFrequencyCap)) return false;
+ KeyedFrequencyCap that = (KeyedFrequencyCap) o;
+ return mMaxCount == that.mMaxCount
+ && mInterval.equals(that.mInterval)
+ && mAdCounterKey.equals(that.mAdCounterKey);
+ }
+
+ /** Returns the hash of the {@link KeyedFrequencyCap} object's data. */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAdCounterKey, mMaxCount, mInterval);
+ }
+
+ @Override
+ public String toString() {
+ return "KeyedFrequencyCap{"
+ + "mAdCounterKey='"
+ + mAdCounterKey
+ + '\''
+ + ", mMaxCount="
+ + mMaxCount
+ + ", mInterval="
+ + mInterval
+ + '}';
+ }
+
+ /** Builder for creating {@link KeyedFrequencyCap} objects. */
+ public static final class Builder {
+ @Nullable private String mAdCounterKey;
+ private int mMaxCount;
+ @Nullable private Duration mInterval;
+
+ public Builder() {}
+
+ /**
+ * Sets the ad counter key the frequency cap applies to.
+ *
+ * <p>See {@link #getAdCounterKey()} for more information.
+ */
+ @NonNull
+ public Builder setAdCounterKey(@NonNull String adCounterKey) {
+ Objects.requireNonNull(adCounterKey, "Ad counter key must not be null");
+ Preconditions.checkStringNotEmpty(adCounterKey, "Ad counter key must not be empty");
+ mAdCounterKey = adCounterKey;
+ return this;
+ }
+
+ /**
+ * Sets the maximum count within the time interval for the frequency cap.
+ *
+ * <p>See {@link #getMaxCount()} for more information.
+ */
+ @NonNull
+ public Builder setMaxCount(int maxCount) {
+ Preconditions.checkArgument(maxCount >= 0, "Max count must be non-negative");
+ mMaxCount = maxCount;
+ return this;
+ }
+
+ /**
+ * Sets the interval, as a {@link Duration} which will be truncated to the nearest second,
+ * over which the frequency cap is calculated.
+ *
+ * <p>See {@link #getInterval()} for more information.
+ */
+ @NonNull
+ public Builder setInterval(@NonNull Duration interval) {
+ Objects.requireNonNull(interval, "Interval must not be null");
+ Preconditions.checkArgument(
+ interval.getSeconds() > 0, "Interval in seconds must be positive and non-zero");
+ mInterval = interval;
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@link KeyedFrequencyCap} instance.
+ *
+ * @throws NullPointerException if the ad counter key or interval are null
+ * @throws IllegalArgumentException if the ad counter key, max count, or interval are
+ * invalid
+ */
+ @NonNull
+ public KeyedFrequencyCap build() throws NullPointerException, IllegalArgumentException {
+ Objects.requireNonNull(mAdCounterKey, "Event key must be set");
+ Preconditions.checkArgument(mMaxCount >= 0, "Max count must be non-negative");
+ Objects.requireNonNull(mInterval, "Interval must not be null");
+
+ return new KeyedFrequencyCap(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/common/SandboxedSdkContextUtils.java b/android-34/android/adservices/common/SandboxedSdkContextUtils.java
new file mode 100644
index 0000000..c138229
--- /dev/null
+++ b/android-34/android/adservices/common/SandboxedSdkContextUtils.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.common;
+
+import android.app.sdksandbox.SandboxedSdkContext;
+import android.content.Context;
+import android.os.Build;
+
+/**
+ * Class containing some utility functions used by other methods within AdServices.
+ *
+ * @hide
+ */
+public final class SandboxedSdkContextUtils {
+ private SandboxedSdkContextUtils() {
+ // Intended to be a utility class that should not be instantiated.
+ }
+
+ /**
+ * Checks if the context is an instance of SandboxedSdkContext.
+ *
+ * @param context the object to check and cast to {@link SandboxedSdkContext}
+ * @return the context object cast to {@link SandboxedSdkContext} if it is an instance of {@link
+ * SandboxedSdkContext}, or {@code null} otherwise.
+ */
+ public static SandboxedSdkContext getAsSandboxedSdkContext(Context context) {
+ // TODO(b/266693417): Replace build version check with SdkLevel.isAtLeastT()
+ if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ return null; // SandboxedSdkContext is only available in T+
+ }
+
+ if (!(context instanceof SandboxedSdkContext)) {
+ return null;
+ }
+
+ return (SandboxedSdkContext) context;
+ }
+}
diff --git a/android-34/android/adservices/customaudience/AddCustomAudienceOverrideRequest.java b/android-34/android/adservices/customaudience/AddCustomAudienceOverrideRequest.java
new file mode 100644
index 0000000..217ee56
--- /dev/null
+++ b/android-34/android/adservices/customaudience/AddCustomAudienceOverrideRequest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.customaudience;
+
+import android.adservices.common.AdSelectionSignals;
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.OutcomeReceiver;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This POJO represents the {@link
+ * TestCustomAudienceManager#overrideCustomAudienceRemoteInfo(AddCustomAudienceOverrideRequest,
+ * Executor, OutcomeReceiver)} request.
+ *
+ * <p>It contains fields {@code buyer} and {@code name} which will serve as the identifier for the
+ * override fields, {@code biddingLogicJs} and {@code trustedBiddingSignals}, which are used during
+ * ad selection instead of querying external servers.
+ */
+public class AddCustomAudienceOverrideRequest {
+ @NonNull private final AdTechIdentifier mBuyer;
+ @NonNull private final String mName;
+ @NonNull private final String mBiddingLogicJs;
+ private final long mBiddingLogicJsVersion;
+ @NonNull private final AdSelectionSignals mTrustedBiddingSignals;
+
+ public AddCustomAudienceOverrideRequest(
+ @NonNull AdTechIdentifier buyer,
+ @NonNull String name,
+ @NonNull String biddingLogicJs,
+ @NonNull AdSelectionSignals trustedBiddingSignals) {
+ this(buyer, name, biddingLogicJs, 0L, trustedBiddingSignals);
+ }
+
+ private AddCustomAudienceOverrideRequest(
+ @NonNull AdTechIdentifier buyer,
+ @NonNull String name,
+ @NonNull String biddingLogicJs,
+ long biddingLogicJsVersion,
+ @NonNull AdSelectionSignals trustedBiddingSignals) {
+ mBuyer = buyer;
+ mName = name;
+ mBiddingLogicJs = biddingLogicJs;
+ mBiddingLogicJsVersion = biddingLogicJsVersion;
+ mTrustedBiddingSignals = trustedBiddingSignals;
+ }
+
+ /** @return an {@link AdTechIdentifier} representing the buyer */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ /** @return name of the custom audience being overridden */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /** @return the override JavaScript result that should be served during ad selection */
+ @NonNull
+ public String getBiddingLogicJs() {
+ return mBiddingLogicJs;
+ }
+
+ /**
+ * Returns the override bidding logic JavaScript version.
+ *
+ * <p>Default to be {@code 0L}, which will fall back to use default version(V1 or V2).
+ *
+ * @hide
+ */
+ public long getBiddingLogicJsVersion() {
+ return mBiddingLogicJsVersion;
+ }
+
+ /** @return the override trusted bidding signals that should be served during ad selection */
+ @NonNull
+ public AdSelectionSignals getTrustedBiddingSignals() {
+ return mTrustedBiddingSignals;
+ }
+
+ /** Builder for {@link AddCustomAudienceOverrideRequest} objects. */
+ public static final class Builder {
+ @Nullable private AdTechIdentifier mBuyer;
+ @Nullable private String mName;
+ @Nullable private String mBiddingLogicJs;
+ private long mBiddingLogicJsVersion;
+ @Nullable private AdSelectionSignals mTrustedBiddingSignals;
+
+ public Builder() {}
+
+ /** Sets the buyer {@link AdTechIdentifier} for the custom audience. */
+ @NonNull
+ public AddCustomAudienceOverrideRequest.Builder setBuyer(@NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer);
+
+ this.mBuyer = buyer;
+ return this;
+ }
+
+ /** Sets the name for the custom audience to be overridden. */
+ @NonNull
+ public AddCustomAudienceOverrideRequest.Builder setName(@NonNull String name) {
+ Objects.requireNonNull(name);
+
+ this.mName = name;
+ return this;
+ }
+
+ /** Sets the trusted bidding signals to be served during ad selection. */
+ @NonNull
+ public AddCustomAudienceOverrideRequest.Builder setTrustedBiddingSignals(
+ @NonNull AdSelectionSignals trustedBiddingSignals) {
+ Objects.requireNonNull(trustedBiddingSignals);
+
+ this.mTrustedBiddingSignals = trustedBiddingSignals;
+ return this;
+ }
+
+ /** Sets the bidding logic JavaScript that should be served during ad selection. */
+ @NonNull
+ public AddCustomAudienceOverrideRequest.Builder setBiddingLogicJs(
+ @NonNull String biddingLogicJs) {
+ Objects.requireNonNull(biddingLogicJs);
+
+ this.mBiddingLogicJs = biddingLogicJs;
+ return this;
+ }
+
+ /**
+ * Sets the bidding logic JavaScript version.
+ *
+ * <p>Default to be {@code 0L}, which will fall back to use default version(V1 or V2).
+ *
+ * @hide
+ */
+ @NonNull
+ public AddCustomAudienceOverrideRequest.Builder setBiddingLogicJsVersion(
+ long biddingLogicJsVersion) {
+ this.mBiddingLogicJsVersion = biddingLogicJsVersion;
+ return this;
+ }
+
+ /** Builds a {@link AddCustomAudienceOverrideRequest} instance. */
+ @NonNull
+ public AddCustomAudienceOverrideRequest build() {
+ Objects.requireNonNull(mBuyer);
+ Objects.requireNonNull(mName);
+ Objects.requireNonNull(mBiddingLogicJs);
+ Objects.requireNonNull(mTrustedBiddingSignals);
+
+ return new AddCustomAudienceOverrideRequest(
+ mBuyer, mName, mBiddingLogicJs, mBiddingLogicJsVersion, mTrustedBiddingSignals);
+ }
+ }
+}
diff --git a/android-34/android/adservices/customaudience/CustomAudience.java b/android-34/android/adservices/customaudience/CustomAudience.java
new file mode 100644
index 0000000..551a1b7
--- /dev/null
+++ b/android-34/android/adservices/customaudience/CustomAudience.java
@@ -0,0 +1,457 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.customaudience;
+
+import android.adservices.common.AdData;
+import android.adservices.common.AdSelectionSignals;
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.adservices.AdServicesParcelableUtil;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents the information necessary for a custom audience to participate in ad selection.
+ *
+ * <p>A custom audience is an abstract grouping of users with similar demonstrated interests. This
+ * class is a collection of some data stored on a device that is necessary to serve advertisements
+ * targeting a single custom audience.
+ */
+public final class CustomAudience implements Parcelable {
+ @NonNull private final AdTechIdentifier mBuyer;
+ @NonNull private final String mName;
+ @Nullable private final Instant mActivationTime;
+ @Nullable private final Instant mExpirationTime;
+ @NonNull private final Uri mDailyUpdateUri;
+ @Nullable private final AdSelectionSignals mUserBiddingSignals;
+ @Nullable private final TrustedBiddingData mTrustedBiddingData;
+ @NonNull private final Uri mBiddingLogicUri;
+ @NonNull private final List<AdData> mAds;
+
+ @NonNull
+ public static final Creator<CustomAudience> CREATOR = new Creator<CustomAudience>() {
+ @Override
+ public CustomAudience createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ return new CustomAudience(in);
+ }
+
+ @Override
+ public CustomAudience[] newArray(int size) {
+ return new CustomAudience[size];
+ }
+ };
+
+ private CustomAudience(@NonNull CustomAudience.Builder builder) {
+ Objects.requireNonNull(builder);
+
+ mBuyer = builder.mBuyer;
+ mName = builder.mName;
+ mActivationTime = builder.mActivationTime;
+ mExpirationTime = builder.mExpirationTime;
+ mDailyUpdateUri = builder.mDailyUpdateUri;
+ mUserBiddingSignals = builder.mUserBiddingSignals;
+ mTrustedBiddingData = builder.mTrustedBiddingData;
+ mBiddingLogicUri = builder.mBiddingLogicUri;
+ mAds = builder.mAds;
+ }
+
+ private CustomAudience(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+
+ mBuyer = AdTechIdentifier.CREATOR.createFromParcel(in);
+ mName = in.readString();
+ mActivationTime =
+ AdServicesParcelableUtil.readNullableFromParcel(
+ in, (sourceParcel) -> Instant.ofEpochMilli(sourceParcel.readLong()));
+ mExpirationTime =
+ AdServicesParcelableUtil.readNullableFromParcel(
+ in, (sourceParcel) -> Instant.ofEpochMilli(sourceParcel.readLong()));
+ mDailyUpdateUri = Uri.CREATOR.createFromParcel(in);
+ mUserBiddingSignals =
+ AdServicesParcelableUtil.readNullableFromParcel(
+ in, AdSelectionSignals.CREATOR::createFromParcel);
+ mTrustedBiddingData =
+ AdServicesParcelableUtil.readNullableFromParcel(
+ in, TrustedBiddingData.CREATOR::createFromParcel);
+ mBiddingLogicUri = Uri.CREATOR.createFromParcel(in);
+ mAds = in.createTypedArrayList(AdData.CREATOR);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ mBuyer.writeToParcel(dest, flags);
+ dest.writeString(mName);
+ AdServicesParcelableUtil.writeNullableToParcel(
+ dest,
+ mActivationTime,
+ (targetParcel, sourceInstant) ->
+ targetParcel.writeLong(sourceInstant.toEpochMilli()));
+ AdServicesParcelableUtil.writeNullableToParcel(
+ dest,
+ mExpirationTime,
+ (targetParcel, sourceInstant) ->
+ targetParcel.writeLong(sourceInstant.toEpochMilli()));
+ mDailyUpdateUri.writeToParcel(dest, flags);
+ AdServicesParcelableUtil.writeNullableToParcel(
+ dest,
+ mUserBiddingSignals,
+ (targetParcel, sourceSignals) -> sourceSignals.writeToParcel(targetParcel, flags));
+ AdServicesParcelableUtil.writeNullableToParcel(
+ dest,
+ mTrustedBiddingData,
+ (targetParcel, sourceData) -> sourceData.writeToParcel(targetParcel, flags));
+ mBiddingLogicUri.writeToParcel(dest, flags);
+ dest.writeTypedList(mAds);
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * A buyer is identified by a domain in the form "buyerexample.com".
+ *
+ * @return an {@link AdTechIdentifier} containing the custom audience's buyer's domain
+ */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ /**
+ * The custom audience's name is an arbitrary string provided by the owner and buyer on creation
+ * of the {@link CustomAudience} object.
+ *
+ * @return the String name of the custom audience
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * On creation of the {@link CustomAudience} object, an optional activation time may be set in
+ * the future, in order to serve a delayed activation. If the field is not set, the {@link
+ * CustomAudience} will be activated at the time of joining.
+ *
+ * <p>For example, a custom audience for lapsed users may not activate until a threshold of
+ * inactivity is reached, at which point the custom audience's ads will participate in the ad
+ * selection process, potentially redirecting lapsed users to the original owner application.
+ *
+ * <p>The maximum delay in activation is 60 days from initial creation.
+ *
+ * <p>If specified, the activation time must be an earlier instant than the expiration time.
+ *
+ * @return the timestamp {@link Instant}, truncated to milliseconds, after which the custom
+ * audience is active
+ */
+ @Nullable
+ public Instant getActivationTime() {
+ return mActivationTime;
+ }
+
+ /**
+ * Once the expiration time has passed, a custom audience is no longer eligible for daily
+ * ad/bidding data updates or participation in the ad selection process. The custom audience
+ * will then be deleted from memory by the next daily update.
+ *
+ * <p>If no expiration time is provided on creation of the {@link CustomAudience}, expiry will
+ * default to 60 days from activation.
+ *
+ * <p>The maximum expiry is 60 days from initial activation.
+ *
+ * @return the timestamp {@link Instant}, truncated to milliseconds, after which the custom
+ * audience should be removed
+ */
+ @Nullable
+ public Instant getExpirationTime() {
+ return mExpirationTime;
+ }
+
+ /**
+ * This URI points to a buyer-operated server that hosts updated bidding data and ads metadata
+ * to be used in the on-device ad selection process. The URI must use HTTPS.
+ *
+ * @return the custom audience's daily update URI
+ */
+ @NonNull
+ public Uri getDailyUpdateUri() {
+ return mDailyUpdateUri;
+ }
+
+ /**
+ * User bidding signals are optionally provided by buyers to be consumed by buyer-provided
+ * JavaScript during ad selection in an isolated execution environment.
+ *
+ * <p>If the user bidding signals are not a valid JSON object that can be consumed by the
+ * buyer's JS, the custom audience will not be eligible for ad selection.
+ *
+ * <p>If not specified, the {@link CustomAudience} will not participate in ad selection until
+ * user bidding signals are provided via the daily update for the custom audience.
+ *
+ * @return an {@link AdSelectionSignals} object representing the user bidding signals for the
+ * custom audience
+ */
+ @Nullable
+ public AdSelectionSignals getUserBiddingSignals() {
+ return mUserBiddingSignals;
+ }
+
+ /**
+ * Trusted bidding data consists of a URI pointing to a trusted server for buyers' bidding data
+ * and a list of keys to query the server with. Note that the keys are arbitrary identifiers
+ * that will only be used to query the trusted server for a buyer's bidding logic during ad
+ * selection.
+ *
+ * <p>If not specified, the {@link CustomAudience} will not participate in ad selection until
+ * trusted bidding data are provided via the daily update for the custom audience.
+ *
+ * @return a {@link TrustedBiddingData} object containing the custom audience's trusted bidding
+ * data
+ */
+ @Nullable
+ public TrustedBiddingData getTrustedBiddingData() {
+ return mTrustedBiddingData;
+ }
+
+ /**
+ * Returns the target URI used to fetch bidding logic when a custom audience participates in the
+ * ad selection process. The URI must use HTTPS.
+ *
+ * @return the URI for fetching buyer bidding logic
+ */
+ @NonNull
+ public Uri getBiddingLogicUri() {
+ return mBiddingLogicUri;
+ }
+
+ /**
+ * This list of {@link AdData} objects is a full and complete list of the ads that will be
+ * served by this {@link CustomAudience} during the ad selection process.
+ *
+ * <p>If not specified, or if an empty list is provided, the {@link CustomAudience} will not
+ * participate in ad selection until a valid list of ads are provided via the daily update for
+ * the custom audience.
+ *
+ * @return a {@link List} of {@link AdData} objects representing ads currently served by the
+ * custom audience
+ */
+ @NonNull
+ public List<AdData> getAds() {
+ return mAds;
+ }
+
+ /**
+ * Checks whether two {@link CustomAudience} objects contain the same information.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CustomAudience)) return false;
+ CustomAudience that = (CustomAudience) o;
+ return mBuyer.equals(that.mBuyer)
+ && mName.equals(that.mName)
+ && Objects.equals(mActivationTime, that.mActivationTime)
+ && Objects.equals(mExpirationTime, that.mExpirationTime)
+ && mDailyUpdateUri.equals(that.mDailyUpdateUri)
+ && Objects.equals(mUserBiddingSignals, that.mUserBiddingSignals)
+ && Objects.equals(mTrustedBiddingData, that.mTrustedBiddingData)
+ && mBiddingLogicUri.equals(that.mBiddingLogicUri)
+ && mAds.equals(that.mAds);
+ }
+
+ /**
+ * Returns the hash of the {@link CustomAudience} object's data.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mBuyer,
+ mName,
+ mActivationTime,
+ mExpirationTime,
+ mDailyUpdateUri,
+ mUserBiddingSignals,
+ mTrustedBiddingData,
+ mBiddingLogicUri,
+ mAds);
+ }
+
+ /** Builder for {@link CustomAudience} objects. */
+ public static final class Builder {
+ @Nullable private AdTechIdentifier mBuyer;
+ @Nullable private String mName;
+ @Nullable private Instant mActivationTime;
+ @Nullable private Instant mExpirationTime;
+ @Nullable private Uri mDailyUpdateUri;
+ @Nullable private AdSelectionSignals mUserBiddingSignals;
+ @Nullable private TrustedBiddingData mTrustedBiddingData;
+ @Nullable private Uri mBiddingLogicUri;
+ @Nullable private List<AdData> mAds;
+
+ // TODO(b/232883403): We may need to add @NonNUll members as args.
+ public Builder() {
+ }
+
+ /**
+ * Sets the buyer {@link AdTechIdentifier}.
+ *
+ * <p>See {@link #getBuyer()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setBuyer(@NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer);
+ mBuyer = buyer;
+ return this;
+ }
+
+ /**
+ * Sets the {@link CustomAudience} object's name.
+ * <p>
+ * See {@link #getName()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setName(@NonNull String name) {
+ Objects.requireNonNull(name);
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Sets the time, truncated to milliseconds, after which the {@link CustomAudience} will
+ * serve ads.
+ *
+ * <p>Set to {@code null} in order for this {@link CustomAudience} to be immediately active
+ * and participate in ad selection.
+ *
+ * <p>See {@link #getActivationTime()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setActivationTime(@Nullable Instant activationTime) {
+ mActivationTime = activationTime;
+ return this;
+ }
+
+ /**
+ * Sets the time, truncated to milliseconds, after which the {@link CustomAudience} should
+ * be removed.
+ * <p>
+ * See {@link #getExpirationTime()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setExpirationTime(@Nullable Instant expirationTime) {
+ mExpirationTime = expirationTime;
+ return this;
+ }
+
+ /**
+ * Sets the daily update URI. The URI must use HTTPS.
+ *
+ * <p>See {@link #getDailyUpdateUri()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setDailyUpdateUri(@NonNull Uri dailyUpdateUri) {
+ Objects.requireNonNull(dailyUpdateUri);
+ mDailyUpdateUri = dailyUpdateUri;
+ return this;
+ }
+
+ /**
+ * Sets the user bidding signals used in the ad selection process.
+ *
+ * <p>See {@link #getUserBiddingSignals()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setUserBiddingSignals(
+ @Nullable AdSelectionSignals userBiddingSignals) {
+ mUserBiddingSignals = userBiddingSignals;
+ return this;
+ }
+
+ /**
+ * Sets the trusted bidding data to be queried and used in the ad selection process.
+ * <p>
+ * See {@link #getTrustedBiddingData()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setTrustedBiddingData(
+ @Nullable TrustedBiddingData trustedBiddingData) {
+ mTrustedBiddingData = trustedBiddingData;
+ return this;
+ }
+
+ /**
+ * Sets the URI to fetch bidding logic from for use in the ad selection process. The URI
+ * must use HTTPS.
+ *
+ * <p>See {@link #getBiddingLogicUri()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setBiddingLogicUri(@NonNull Uri biddingLogicUri) {
+ Objects.requireNonNull(biddingLogicUri);
+ mBiddingLogicUri = biddingLogicUri;
+ return this;
+ }
+
+ /**
+ * Sets the initial remarketing ads served by the custom audience. Will be assigned with an
+ * empty list if not provided.
+ *
+ * <p>See {@link #getAds()} for more information.
+ */
+ @NonNull
+ public CustomAudience.Builder setAds(@Nullable List<AdData> ads) {
+ mAds = ads;
+ return this;
+ }
+
+ /**
+ * Builds an instance of a {@link CustomAudience}.
+ *
+ * @throws NullPointerException if any non-null parameter is null
+ * @throws IllegalArgumentException if the expiration time occurs before activation time
+ * @throws IllegalArgumentException if the expiration time is set before the current time
+ */
+ @NonNull
+ public CustomAudience build() {
+ Objects.requireNonNull(mBuyer);
+ Objects.requireNonNull(mName);
+ Objects.requireNonNull(mDailyUpdateUri);
+ Objects.requireNonNull(mBiddingLogicUri);
+
+ // To pass the API lint, we should not allow null Collection.
+ if (mAds == null) {
+ mAds = List.of();
+ }
+
+ return new CustomAudience(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/customaudience/CustomAudienceManager.java b/android-34/android/adservices/customaudience/CustomAudienceManager.java
new file mode 100644
index 0000000..00c2c42
--- /dev/null
+++ b/android-34/android/adservices/customaudience/CustomAudienceManager.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.customaudience;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE;
+
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.common.FledgeErrorResponse;
+import android.adservices.common.SandboxedSdkContextUtils;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.app.sdksandbox.SandboxedSdkContext;
+import android.content.Context;
+import android.os.Build;
+import android.os.LimitExceededException;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.LoggerFactory;
+import com.android.adservices.ServiceBinder;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/** CustomAudienceManager provides APIs for app and ad-SDKs to join / leave custom audiences. */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public class CustomAudienceManager {
+ private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
+ /**
+ * Constant that represents the service name for {@link CustomAudienceManager} to be used in
+ * {@link android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers}
+ *
+ * @hide
+ */
+ public static final String CUSTOM_AUDIENCE_SERVICE = "custom_audience_service";
+
+ @NonNull private Context mContext;
+ @NonNull private ServiceBinder<ICustomAudienceService> mServiceBinder;
+
+ /**
+ * Factory method for creating an instance of CustomAudienceManager.
+ *
+ * @param context The {@link Context} to use
+ * @return A {@link CustomAudienceManager} instance
+ */
+ @NonNull
+ public static CustomAudienceManager get(@NonNull Context context) {
+ // On T+, context.getSystemService() does more than just call constructor.
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ ? context.getSystemService(CustomAudienceManager.class)
+ : new CustomAudienceManager(context);
+ }
+
+ /**
+ * Create a service binder CustomAudienceManager
+ *
+ * @hide
+ */
+ public CustomAudienceManager(@NonNull Context context) {
+ Objects.requireNonNull(context);
+
+ // In case the CustomAudienceManager is initiated from inside a sdk_sandbox process the
+ // fields will be immediately rewritten by the initialize method below.
+ initialize(context);
+ }
+
+ /**
+ * Initializes {@link CustomAudienceManager} with the given {@code context}.
+ *
+ * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context.
+ * For more information check the javadoc on the {@link
+ * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}.
+ *
+ * @hide
+ * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry
+ */
+ public CustomAudienceManager initialize(@NonNull Context context) {
+ Objects.requireNonNull(context);
+
+ mContext = context;
+ mServiceBinder =
+ ServiceBinder.getServiceBinder(
+ context,
+ AdServicesCommon.ACTION_CUSTOM_AUDIENCE_SERVICE,
+ ICustomAudienceService.Stub::asInterface);
+ return this;
+ }
+
+ /** Create a service with test-enabling APIs */
+ @NonNull
+ public TestCustomAudienceManager getTestCustomAudienceManager() {
+ return new TestCustomAudienceManager(this, getCallerPackageName());
+ }
+
+ @NonNull
+ ICustomAudienceService getService() {
+ ICustomAudienceService service = mServiceBinder.getService();
+ Objects.requireNonNull(service);
+ return service;
+ }
+
+ /**
+ * Adds the user to the given {@link CustomAudience}.
+ *
+ * <p>An attempt to register the user for a custom audience with the same combination of {@code
+ * ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
+ * information to be overwritten, including the list of ads data.
+ *
+ * <p>Note that the ads list can be completely overwritten by the daily background fetch job.
+ *
+ * <p>This call fails with an {@link SecurityException} if
+ *
+ * <ol>
+ * <li>the {@code ownerPackageName} is not calling app's package name and/or
+ * <li>the buyer is not authorized to use the API.
+ * </ol>
+ *
+ * <p>This call fails with an {@link IllegalArgumentException} if
+ *
+ * <ol>
+ * <li>the storage limit has been exceeded by the calling application and/or
+ * <li>any URI parameters in the {@link CustomAudience} given are not authenticated with the
+ * {@link CustomAudience} buyer.
+ * </ol>
+ *
+ * <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
+ * allowed rate limits and is throttled.
+ *
+ * <p>This call fails with an {@link IllegalStateException} if an internal service error is
+ * encountered.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void joinCustomAudience(
+ @NonNull JoinCustomAudienceRequest joinCustomAudienceRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(joinCustomAudienceRequest);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ final CustomAudience customAudience = joinCustomAudienceRequest.getCustomAudience();
+
+ try {
+ final ICustomAudienceService service = getService();
+
+ service.joinCustomAudience(
+ customAudience,
+ getCallerPackageName(),
+ new ICustomAudienceCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Internal Error!", e));
+ }
+ }
+
+ /**
+ * Attempts to remove a user from a custom audience by deleting any existing {@link
+ * CustomAudience} data, identified by {@code ownerPackageName}, {@code buyer}, and {@code
+ * name}.
+ *
+ * <p>This call fails with an {@link SecurityException} if
+ *
+ * <ol>
+ * <li>the {@code ownerPackageName} is not calling app's package name; and/or
+ * <li>the buyer is not authorized to use the API.
+ * </ol>
+ *
+ * <p>This call fails with {@link LimitExceededException} if the calling package exceeds the
+ * allowed rate limits and is throttled.
+ *
+ * <p>This call does not inform the caller whether the custom audience specified existed in
+ * on-device storage. In other words, it will fail silently when a buyer attempts to leave a
+ * custom audience that was not joined.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void leaveCustomAudience(
+ @NonNull LeaveCustomAudienceRequest leaveCustomAudienceRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(leaveCustomAudienceRequest);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+
+ final AdTechIdentifier buyer = leaveCustomAudienceRequest.getBuyer();
+ final String name = leaveCustomAudienceRequest.getName();
+
+ try {
+ final ICustomAudienceService service = getService();
+
+ service.leaveCustomAudience(
+ getCallerPackageName(),
+ buyer,
+ name,
+ new ICustomAudienceCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Internal Error!", e));
+ }
+ }
+
+ private String getCallerPackageName() {
+ SandboxedSdkContext sandboxedSdkContext =
+ SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
+ return sandboxedSdkContext == null
+ ? mContext.getPackageName()
+ : sandboxedSdkContext.getClientPackageName();
+ }
+}
diff --git a/android-34/android/adservices/customaudience/JoinCustomAudienceRequest.java b/android-34/android/adservices/customaudience/JoinCustomAudienceRequest.java
new file mode 100644
index 0000000..f215fd2
--- /dev/null
+++ b/android-34/android/adservices/customaudience/JoinCustomAudienceRequest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.customaudience;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * The request object to join a custom audience.
+ */
+public class JoinCustomAudienceRequest {
+ @NonNull
+ private final CustomAudience mCustomAudience;
+
+ private JoinCustomAudienceRequest(@NonNull JoinCustomAudienceRequest.Builder builder) {
+ mCustomAudience = builder.mCustomAudience;
+ }
+
+ /**
+ * Returns the custom audience to join.
+ */
+ @NonNull
+ public CustomAudience getCustomAudience() {
+ return mCustomAudience;
+ }
+
+ /**
+ * Checks whether two {@link JoinCustomAudienceRequest} objects contain the same information.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof JoinCustomAudienceRequest)) return false;
+ JoinCustomAudienceRequest that = (JoinCustomAudienceRequest) o;
+ return mCustomAudience.equals(that.mCustomAudience);
+ }
+
+ /**
+ * Returns the hash of the {@link JoinCustomAudienceRequest} object's data.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCustomAudience);
+ }
+
+ /** Builder for {@link JoinCustomAudienceRequest} objects. */
+ public static final class Builder {
+ @Nullable private CustomAudience mCustomAudience;
+
+ public Builder() {
+ }
+
+ /**
+ * Sets the custom audience to join.
+ *
+ * <p>See {@link #getCustomAudience()} for more information.
+ */
+ @NonNull
+ public JoinCustomAudienceRequest.Builder setCustomAudience(
+ @NonNull CustomAudience customAudience) {
+ Objects.requireNonNull(customAudience);
+ mCustomAudience = customAudience;
+ return this;
+ }
+
+ /**
+ * Builds an instance of a {@link JoinCustomAudienceRequest}.
+ *
+ * @throws NullPointerException if any non-null parameter is null
+ */
+ @NonNull
+ public JoinCustomAudienceRequest build() {
+ Objects.requireNonNull(mCustomAudience);
+
+ return new JoinCustomAudienceRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/customaudience/LeaveCustomAudienceRequest.java b/android-34/android/adservices/customaudience/LeaveCustomAudienceRequest.java
new file mode 100644
index 0000000..b7d77ef
--- /dev/null
+++ b/android-34/android/adservices/customaudience/LeaveCustomAudienceRequest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.customaudience;
+
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Objects;
+
+/** The request object is used to leave a custom audience. */
+public final class LeaveCustomAudienceRequest {
+ @NonNull private final AdTechIdentifier mBuyer;
+ @NonNull private final String mName;
+
+ private LeaveCustomAudienceRequest(@NonNull LeaveCustomAudienceRequest.Builder builder) {
+ mBuyer = builder.mBuyer;
+ mName = builder.mName;
+ }
+
+ /**
+ * Gets the buyer's {@link AdTechIdentifier}, as identified by a domain in the form
+ * "buyerexample.com".
+ *
+ * @return an {@link AdTechIdentifier} containing the custom audience's buyer's domain
+ */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ /**
+ * Gets the arbitrary string provided by the owner and buyer on creation of the {@link
+ * CustomAudience} object that represents a single custom audience.
+ *
+ * @return the String name of the custom audience
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Checks whether two {@link LeaveCustomAudienceRequest} objects contain the same information.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof LeaveCustomAudienceRequest)) return false;
+ LeaveCustomAudienceRequest that = (LeaveCustomAudienceRequest) o;
+ return mBuyer.equals(that.mBuyer) && mName.equals(that.mName);
+ }
+
+ /**
+ * Returns the hash of the {@link LeaveCustomAudienceRequest} object's data.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mBuyer, mName);
+ }
+
+ /** Builder for {@link LeaveCustomAudienceRequest} objects. */
+ public static final class Builder {
+ @Nullable private AdTechIdentifier mBuyer;
+ @Nullable private String mName;
+
+ public Builder() {}
+
+ /**
+ * Sets the buyer {@link AdTechIdentifier}.
+ *
+ * <p>See {@link #getBuyer()} for more information.
+ */
+ @NonNull
+ public LeaveCustomAudienceRequest.Builder setBuyer(@NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer);
+ mBuyer = buyer;
+ return this;
+ }
+
+ /**
+ * Sets the {@link CustomAudience} object's name.
+ * <p>
+ * See {@link #getName()} for more information.
+ */
+ @NonNull
+ public LeaveCustomAudienceRequest.Builder setName(@NonNull String name) {
+ Objects.requireNonNull(name);
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Builds an instance of a {@link LeaveCustomAudienceRequest}.
+ *
+ * @throws NullPointerException if any non-null parameter is null
+ */
+ @NonNull
+ public LeaveCustomAudienceRequest build() {
+ Objects.requireNonNull(mBuyer);
+ Objects.requireNonNull(mName);
+
+ return new LeaveCustomAudienceRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/customaudience/RemoveCustomAudienceOverrideRequest.java b/android-34/android/adservices/customaudience/RemoveCustomAudienceOverrideRequest.java
new file mode 100644
index 0000000..2996129
--- /dev/null
+++ b/android-34/android/adservices/customaudience/RemoveCustomAudienceOverrideRequest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.customaudience;
+
+import android.adservices.common.AdTechIdentifier;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.OutcomeReceiver;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This POJO represents the {@link TestCustomAudienceManager#removeCustomAudienceRemoteInfoOverride(
+ * RemoveCustomAudienceOverrideRequest, Executor, OutcomeReceiver)} request.
+ *
+ * <p>It contains fields {@code buyer} and {@code name} which will serve as the identifier for the
+ * overrides to be removed.
+ */
+public class RemoveCustomAudienceOverrideRequest {
+ @NonNull private final AdTechIdentifier mBuyer;
+ @NonNull private final String mName;
+
+ public RemoveCustomAudienceOverrideRequest(
+ @NonNull AdTechIdentifier buyer,
+ @NonNull String name) {
+ mBuyer = buyer;
+ mName = name;
+ }
+
+ /** @return an {@link AdTechIdentifier} representing the buyer */
+ @NonNull
+ public AdTechIdentifier getBuyer() {
+ return mBuyer;
+ }
+
+ /** @return name of the custom audience being overridden */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /** Builder for {@link RemoveCustomAudienceOverrideRequest} objects. */
+ public static final class Builder {
+ @Nullable private AdTechIdentifier mBuyer;
+ @Nullable private String mName;
+
+ public Builder() {}
+
+ /** Sets the buyer {@link AdTechIdentifier} for the custom audience. */
+ @NonNull
+ public RemoveCustomAudienceOverrideRequest.Builder setBuyer(
+ @NonNull AdTechIdentifier buyer) {
+ Objects.requireNonNull(buyer);
+
+ this.mBuyer = buyer;
+ return this;
+ }
+
+ /** Sets the name for the custom audience that was overridden. */
+ @NonNull
+ public RemoveCustomAudienceOverrideRequest.Builder setName(@NonNull String name) {
+ Objects.requireNonNull(name);
+
+ this.mName = name;
+ return this;
+ }
+
+ /** Builds a {@link RemoveCustomAudienceOverrideRequest} instance. */
+ @NonNull
+ public RemoveCustomAudienceOverrideRequest build() {
+ Objects.requireNonNull(mBuyer);
+ Objects.requireNonNull(mName);
+
+ return new RemoveCustomAudienceOverrideRequest(mBuyer, mName);
+ }
+ }
+}
diff --git a/android-34/android/adservices/customaudience/TestCustomAudienceManager.java b/android-34/android/adservices/customaudience/TestCustomAudienceManager.java
new file mode 100644
index 0000000..46251cf
--- /dev/null
+++ b/android-34/android/adservices/customaudience/TestCustomAudienceManager.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.customaudience;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE;
+
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.FledgeErrorResponse;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.os.Build;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.LoggerFactory;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/** TestCustomAudienceManager provides APIs for app and ad-SDKs to test custom audiences. */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public class TestCustomAudienceManager {
+ private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
+
+ private final CustomAudienceManager mCustomAudienceManager;
+ private final String mCallerPackageName;
+
+ TestCustomAudienceManager(
+ @NonNull CustomAudienceManager customAudienceManager,
+ @NonNull String callerPackageName) {
+ Objects.requireNonNull(customAudienceManager);
+ Objects.requireNonNull(callerPackageName);
+
+ mCustomAudienceManager = customAudienceManager;
+ mCallerPackageName = callerPackageName;
+ }
+
+ /**
+ * Overrides the Custom Audience API to avoid fetching data from remote servers and use the data
+ * provided in {@link AddCustomAudienceOverrideRequest} instead. The {@link
+ * AddCustomAudienceOverrideRequest} is provided by the Ads SDK.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * <p>This call will fail silently if the {@code owner} in the {@code request} is not the
+ * calling app's package name.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The receiver either returns a {@code void} for a successful run, or an {@link
+ * Exception} indicates the error.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void overrideCustomAudienceRemoteInfo(
+ @NonNull AddCustomAudienceOverrideRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+ try {
+ final ICustomAudienceService service = mCustomAudienceManager.getService();
+ service.overrideCustomAudienceRemoteInfo(
+ mCallerPackageName,
+ request.getBuyer(),
+ request.getName(),
+ request.getBiddingLogicJs(),
+ request.getBiddingLogicJsVersion(),
+ request.getTrustedBiddingSignals(),
+ new CustomAudienceOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Internal Error!", e));
+ }
+ }
+ /**
+ * Removes an override in th Custom Audience API with associated the data in {@link
+ * RemoveCustomAudienceOverrideRequest}.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The {@link RemoveCustomAudienceOverrideRequest} is provided by the Ads SDK. The
+ * receiver either returns a {@code void} for a successful run, or an {@link Exception}
+ * indicates the error.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void removeCustomAudienceRemoteInfoOverride(
+ @NonNull RemoveCustomAudienceOverrideRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+ try {
+ final ICustomAudienceService service = mCustomAudienceManager.getService();
+ service.removeCustomAudienceRemoteInfoOverride(
+ mCallerPackageName,
+ request.getBuyer(),
+ request.getName(),
+ new CustomAudienceOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Internal Error!", e));
+ }
+ }
+ /**
+ * Removes all override data in the Custom Audience API.
+ *
+ * <p>This method is intended to be used for end-to-end testing. This API is enabled only for
+ * apps in debug mode with developer options enabled.
+ *
+ * @throws IllegalStateException if this API is not enabled for the caller
+ * <p>The receiver either returns a {@code void} for a successful run, or an {@link
+ * Exception} indicates the error.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+ public void resetAllCustomAudienceOverrides(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> receiver) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+ try {
+ final ICustomAudienceService service = mCustomAudienceManager.getService();
+ service.resetAllCustomAudienceOverrides(
+ new CustomAudienceOverrideCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ executor.execute(() -> receiver.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(FledgeErrorResponse failureParcel) {
+ executor.execute(
+ () ->
+ receiver.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ });
+ } catch (RemoteException e) {
+ sLogger.e(e, "Exception");
+ receiver.onError(new IllegalStateException("Internal Error!", e));
+ }
+ }
+}
diff --git a/android-34/android/adservices/customaudience/TrustedBiddingData.java b/android-34/android/adservices/customaudience/TrustedBiddingData.java
new file mode 100644
index 0000000..0143a13
--- /dev/null
+++ b/android-34/android/adservices/customaudience/TrustedBiddingData.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.customaudience;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents data used during the ad selection process to fetch buyer bidding signals from a
+ * trusted key/value server. The fetched data is used during the ad selection process and consumed
+ * by buyer JavaScript logic running in an isolated execution environment.
+ */
+public final class TrustedBiddingData implements Parcelable {
+ @NonNull private final Uri mTrustedBiddingUri;
+ @NonNull
+ private final List<String> mTrustedBiddingKeys;
+
+ @NonNull
+ public static final Creator<TrustedBiddingData> CREATOR = new Creator<TrustedBiddingData>() {
+ @Override
+ public TrustedBiddingData createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new TrustedBiddingData(in);
+ }
+
+ @Override
+ public TrustedBiddingData[] newArray(int size) {
+ return new TrustedBiddingData[size];
+ }
+ };
+
+ private TrustedBiddingData(@NonNull TrustedBiddingData.Builder builder) {
+ mTrustedBiddingUri = builder.mTrustedBiddingUri;
+ mTrustedBiddingKeys = builder.mTrustedBiddingKeys;
+ }
+
+ private TrustedBiddingData(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ mTrustedBiddingUri = Uri.CREATOR.createFromParcel(in);
+ mTrustedBiddingKeys = in.createStringArrayList();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ mTrustedBiddingUri.writeToParcel(dest, flags);
+ dest.writeStringList(mTrustedBiddingKeys);
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @return the URI pointing to the trusted key-value server holding bidding signals. The URI
+ * must use HTTPS.
+ */
+ @NonNull
+ public Uri getTrustedBiddingUri() {
+ return mTrustedBiddingUri;
+ }
+
+ /**
+ * @return the list of keys to query from the trusted key-value server holding bidding signals
+ */
+ @NonNull
+ public List<String> getTrustedBiddingKeys() {
+ return mTrustedBiddingKeys;
+ }
+
+ /**
+ * @return {@code true} if two {@link TrustedBiddingData} objects contain the same information
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TrustedBiddingData)) return false;
+ TrustedBiddingData that = (TrustedBiddingData) o;
+ return mTrustedBiddingUri.equals(that.mTrustedBiddingUri)
+ && mTrustedBiddingKeys.equals(that.mTrustedBiddingKeys);
+ }
+
+ /**
+ * @return the hash of the {@link TrustedBiddingData} object's data
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTrustedBiddingUri, mTrustedBiddingKeys);
+ }
+
+ /** Builder for {@link TrustedBiddingData} objects. */
+ public static final class Builder {
+ @Nullable private Uri mTrustedBiddingUri;
+ @Nullable private List<String> mTrustedBiddingKeys;
+
+ // TODO(b/232883403): We may need to add @NonNUll members as args.
+ public Builder() {
+ }
+
+ /**
+ * Sets the URI pointing to a trusted key-value server used to fetch bidding signals during
+ * the ad selection process. The URI must use HTTPS.
+ */
+ @NonNull
+ public Builder setTrustedBiddingUri(@NonNull Uri trustedBiddingUri) {
+ Objects.requireNonNull(trustedBiddingUri);
+ mTrustedBiddingUri = trustedBiddingUri;
+ return this;
+ }
+
+ /**
+ * Sets the list of keys to query the trusted key-value server with.
+ * <p>
+ * This list is permitted to be empty, but it must not be null.
+ */
+ @NonNull
+ public Builder setTrustedBiddingKeys(@NonNull List<String> trustedBiddingKeys) {
+ Objects.requireNonNull(trustedBiddingKeys);
+ mTrustedBiddingKeys = trustedBiddingKeys;
+ return this;
+ }
+
+ /**
+ * Builds the {@link TrustedBiddingData} object.
+ *
+ * @throws NullPointerException if any parameters are null when built
+ */
+ @NonNull
+ public TrustedBiddingData build() {
+ Objects.requireNonNull(mTrustedBiddingUri);
+ // Note that the list of keys is allowed to be empty, but not null
+ Objects.requireNonNull(mTrustedBiddingKeys);
+
+ return new TrustedBiddingData(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/exceptions/AdServicesException.java b/android-34/android/adservices/exceptions/AdServicesException.java
new file mode 100644
index 0000000..fe0d933
--- /dev/null
+++ b/android-34/android/adservices/exceptions/AdServicesException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.exceptions;
+import android.annotation.Nullable;
+/**
+ * Exception thrown by AdServices.
+ */
+public class AdServicesException extends Exception {
+ public AdServicesException(@Nullable String message, @Nullable Throwable e) {
+ super(message, e);
+ }
+ public AdServicesException(@Nullable String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/android-34/android/adservices/measurement/DeletionParam.java b/android-34/android/adservices/measurement/DeletionParam.java
new file mode 100644
index 0000000..00d5fad
--- /dev/null
+++ b/android-34/android/adservices/measurement/DeletionParam.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.measurement;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Class to hold deletion related request. This is an internal class for communication between the
+ * {@link MeasurementManager} and {@link IMeasurementService} impl.
+ *
+ * @hide
+ */
+public final class DeletionParam implements Parcelable {
+ private final List<Uri> mOriginUris;
+ private final List<Uri> mDomainUris;
+ private final Instant mStart;
+ private final Instant mEnd;
+ private final String mAppPackageName;
+ private final String mSdkPackageName;
+ @DeletionRequest.DeletionMode private final int mDeletionMode;
+ @DeletionRequest.MatchBehavior private final int mMatchBehavior;
+
+ private DeletionParam(@NonNull Builder builder) {
+ mOriginUris = builder.mOriginUris;
+ mDomainUris = builder.mDomainUris;
+ mDeletionMode = builder.mDeletionMode;
+ mMatchBehavior = builder.mMatchBehavior;
+ mStart = builder.mStart;
+ mEnd = builder.mEnd;
+ mAppPackageName = builder.mAppPackageName;
+ mSdkPackageName = builder.mSdkPackageName;
+ }
+
+ /** Unpack an DeletionRequest from a Parcel. */
+ private DeletionParam(Parcel in) {
+ mAppPackageName = in.readString();
+ mSdkPackageName = in.readString();
+
+ mDomainUris = new ArrayList<>();
+ in.readTypedList(mDomainUris, Uri.CREATOR);
+
+ mOriginUris = new ArrayList<>();
+ in.readTypedList(mOriginUris, Uri.CREATOR);
+
+ boolean hasStart = in.readBoolean();
+ if (hasStart) {
+ mStart = Instant.parse(in.readString());
+ } else {
+ mStart = null;
+ }
+
+ boolean hasEnd = in.readBoolean();
+ if (hasEnd) {
+ mEnd = Instant.parse(in.readString());
+ } else {
+ mEnd = null;
+ }
+
+ mDeletionMode = in.readInt();
+ mMatchBehavior = in.readInt();
+ }
+
+ /** Creator for Parcelable (via reflection). */
+ @NonNull
+ public static final Parcelable.Creator<DeletionParam> CREATOR =
+ new Parcelable.Creator<DeletionParam>() {
+ @Override
+ public DeletionParam createFromParcel(Parcel in) {
+ return new DeletionParam(in);
+ }
+
+ @Override
+ public DeletionParam[] newArray(int size) {
+ return new DeletionParam[size];
+ }
+ };
+
+ /** For Parcelable, no special marshalled objects. */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** For Parcelable, write out to a Parcel in particular order. */
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ out.writeString(mAppPackageName);
+ out.writeString(mSdkPackageName);
+
+ out.writeTypedList(mDomainUris);
+
+ out.writeTypedList(mOriginUris);
+
+ if (mStart != null) {
+ out.writeBoolean(true);
+ out.writeString(mStart.toString());
+ } else {
+ out.writeBoolean(false);
+ }
+
+ if (mEnd != null) {
+ out.writeBoolean(true);
+ out.writeString(mEnd.toString());
+ } else {
+ out.writeBoolean(false);
+ }
+
+ out.writeInt(mDeletionMode);
+
+ out.writeInt(mMatchBehavior);
+ }
+
+ /**
+ * Publisher/Advertiser Origins for which data should be deleted. These will be matched as-is.
+ */
+ @NonNull
+ public List<Uri> getOriginUris() {
+ return mOriginUris;
+ }
+
+ /**
+ * Publisher/Advertiser domains for which data should be deleted. These will be pattern matched
+ * with regex SCHEME://(.*\.|)SITE .
+ */
+ @NonNull
+ public List<Uri> getDomainUris() {
+ return mDomainUris;
+ }
+
+ /** Deletion mode for matched records. */
+ @DeletionRequest.DeletionMode
+ public int getDeletionMode() {
+ return mDeletionMode;
+ }
+
+ /** Match behavior for provided origins/domains. */
+ @DeletionRequest.MatchBehavior
+ public int getMatchBehavior() {
+ return mMatchBehavior;
+ }
+
+ /**
+ * Instant in time the deletion starts, or {@link java.time.Instant#MIN} if starting at the
+ * oldest possible time.
+ */
+ @NonNull
+ public Instant getStart() {
+ return mStart;
+ }
+
+ /**
+ * Instant in time the deletion ends, or {@link java.time.Instant#MAX} if ending at the most
+ * recent time.
+ */
+ @NonNull
+ public Instant getEnd() {
+ return mEnd;
+ }
+
+ /** Package name of the app used for the deletion. */
+ @NonNull
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /** Package name of the sdk used for the deletion. */
+ @NonNull
+ public String getSdkPackageName() {
+ return mSdkPackageName;
+ }
+
+ /** A builder for {@link DeletionParam}. */
+ public static final class Builder {
+ private final List<Uri> mOriginUris;
+ private final List<Uri> mDomainUris;
+ private final Instant mStart;
+ private final Instant mEnd;
+ private final String mAppPackageName;
+ private final String mSdkPackageName;
+ @DeletionRequest.DeletionMode private int mDeletionMode;
+ @DeletionRequest.MatchBehavior private int mMatchBehavior;
+
+ /**
+ * Builder constructor for {@link DeletionParam}.
+ *
+ * @param originUris see {@link DeletionParam#getOriginUris()}
+ * @param domainUris see {@link DeletionParam#getDomainUris()}
+ * @param start see {@link DeletionParam#getStart()}
+ * @param end see {@link DeletionParam#getEnd()}
+ * @param appPackageName see {@link DeletionParam#getAppPackageName()}
+ * @param sdkPackageName see {@link DeletionParam#getSdkPackageName()}
+ */
+ public Builder(
+ @NonNull List<Uri> originUris,
+ @NonNull List<Uri> domainUris,
+ @NonNull Instant start,
+ @NonNull Instant end,
+ @NonNull String appPackageName,
+ @NonNull String sdkPackageName) {
+ Objects.requireNonNull(originUris);
+ Objects.requireNonNull(domainUris);
+ Objects.requireNonNull(start);
+ Objects.requireNonNull(end);
+ Objects.requireNonNull(appPackageName);
+ Objects.requireNonNull(sdkPackageName);
+
+ mOriginUris = originUris;
+ mDomainUris = domainUris;
+ mStart = start;
+ mEnd = end;
+ mAppPackageName = appPackageName;
+ mSdkPackageName = sdkPackageName;
+ }
+
+ /** See {@link DeletionParam#getDeletionMode()}. */
+ @NonNull
+ public Builder setDeletionMode(@DeletionRequest.DeletionMode int deletionMode) {
+ mDeletionMode = deletionMode;
+ return this;
+ }
+
+ /** See {@link DeletionParam#getDeletionMode()}. */
+ @NonNull
+ public Builder setMatchBehavior(@DeletionRequest.MatchBehavior int matchBehavior) {
+ mMatchBehavior = matchBehavior;
+ return this;
+ }
+
+ /** Build the DeletionRequest. */
+ @NonNull
+ public DeletionParam build() {
+ return new DeletionParam(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/DeletionRequest.java b/android-34/android/adservices/measurement/DeletionRequest.java
new file mode 100644
index 0000000..e8e384a
--- /dev/null
+++ b/android-34/android/adservices/measurement/DeletionRequest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** Deletion Request. */
+public class DeletionRequest {
+
+ /**
+ * Deletion modes for matched records.
+ *
+ * @hide
+ */
+ @IntDef(value = {DELETION_MODE_ALL, DELETION_MODE_EXCLUDE_INTERNAL_DATA})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeletionMode {}
+
+ /**
+ * Matching Behaviors for params.
+ *
+ * @hide
+ */
+ @IntDef(value = {MATCH_BEHAVIOR_DELETE, MATCH_BEHAVIOR_PRESERVE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MatchBehavior {}
+
+ /** Deletion mode to delete all data associated with the selected records. */
+ public static final int DELETION_MODE_ALL = 0;
+
+ /**
+ * Deletion mode to delete all data except the internal data (e.g. rate limits) for the selected
+ * records.
+ */
+ public static final int DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1;
+
+ /** Match behavior option to delete the supplied params (Origin/Domains). */
+ public static final int MATCH_BEHAVIOR_DELETE = 0;
+
+ /**
+ * Match behavior option to preserve the supplied params (Origin/Domains) and delete everything
+ * else.
+ */
+ public static final int MATCH_BEHAVIOR_PRESERVE = 1;
+
+ private final Instant mStart;
+ private final Instant mEnd;
+ private final List<Uri> mOriginUris;
+ private final List<Uri> mDomainUris;
+ private final @MatchBehavior int mMatchBehavior;
+ private final @DeletionMode int mDeletionMode;
+
+ private DeletionRequest(@NonNull Builder builder) {
+ mOriginUris = builder.mOriginUris;
+ mDomainUris = builder.mDomainUris;
+ mMatchBehavior = builder.mMatchBehavior;
+ mDeletionMode = builder.mDeletionMode;
+ mStart = builder.mStart;
+ mEnd = builder.mEnd;
+ }
+
+ /** Get the list of origin URIs. */
+ @NonNull
+ public List<Uri> getOriginUris() {
+ return mOriginUris;
+ }
+
+ /** Get the list of domain URIs. */
+ @NonNull
+ public List<Uri> getDomainUris() {
+ return mDomainUris;
+ }
+
+ /** Get the deletion mode. */
+ public @DeletionMode int getDeletionMode() {
+ return mDeletionMode;
+ }
+
+ /** Get the match behavior. */
+ public @MatchBehavior int getMatchBehavior() {
+ return mMatchBehavior;
+ }
+
+ /** Get the start of the deletion range. */
+ @NonNull
+ public Instant getStart() {
+ return mStart;
+ }
+
+ /** Get the end of the deletion range. */
+ @NonNull
+ public Instant getEnd() {
+ return mEnd;
+ }
+
+ /** Builder for {@link DeletionRequest} objects. */
+ public static final class Builder {
+ private Instant mStart = Instant.MIN;
+ private Instant mEnd = Instant.MAX;
+ private List<Uri> mOriginUris;
+ private List<Uri> mDomainUris;
+ @MatchBehavior private int mMatchBehavior;
+ @DeletionMode private int mDeletionMode;
+
+ public Builder() {}
+
+ /**
+ * Set the list of origin URI which will be used for matching. These will be matched with
+ * records using the same origin only, i.e. subdomains won't match. E.g. If originUri is
+ * {@code https://a.example.com}, then {@code https://a.example.com} will match; {@code
+ * https://example.com}, {@code https://b.example.com} and {@code https://abcexample.com}
+ * will NOT match. A null or empty list will match everything.
+ */
+ public @NonNull Builder setOriginUris(@Nullable List<Uri> originUris) {
+ mOriginUris = originUris;
+ return this;
+ }
+
+ /**
+ * Set the list of domain URI which will be used for matching. These will be matched with
+ * records using the same domain or any subdomains. E.g. If domainUri is {@code
+ * https://example.com}, then {@code https://a.example.com}, {@code https://example.com} and
+ * {@code https://b.example.com} will match; {@code https://abcexample.com} will NOT match.
+ * A null or empty list will match everything.
+ */
+ public @NonNull Builder setDomainUris(@Nullable List<Uri> domainUris) {
+ mDomainUris = domainUris;
+ return this;
+ }
+
+ /**
+ * Set the match behavior for the supplied params. {@link #MATCH_BEHAVIOR_DELETE}: This
+ * option will use the supplied params (Origin URIs & Domain URIs) for selecting records for
+ * deletion. {@link #MATCH_BEHAVIOR_PRESERVE}: This option will preserve the data associated
+ * with the supplied params (Origin URIs & Domain URIs) and select remaining records for
+ * deletion.
+ */
+ public @NonNull Builder setMatchBehavior(@MatchBehavior int matchBehavior) {
+ mMatchBehavior = matchBehavior;
+ return this;
+ }
+
+ /**
+ * Set the match behavior for the supplied params. {@link #DELETION_MODE_ALL}: All data
+ * associated with the selected records will be deleted. {@link
+ * #DELETION_MODE_EXCLUDE_INTERNAL_DATA}: All data except the internal system data (e.g.
+ * rate limits) associated with the selected records will be deleted.
+ */
+ public @NonNull Builder setDeletionMode(@DeletionMode int deletionMode) {
+ mDeletionMode = deletionMode;
+ return this;
+ }
+
+ /**
+ * Set the start of the deletion range. Passing in {@link java.time.Instant#MIN} will cause
+ * everything from the oldest record to the specified end be deleted. No set start will
+ * default to {@link java.time.Instant#MIN}.
+ */
+ public @NonNull Builder setStart(@NonNull Instant start) {
+ Objects.requireNonNull(start);
+ mStart = start;
+ return this;
+ }
+
+ /**
+ * Set the end of the deletion range. Passing in {@link java.time.Instant#MAX} will cause
+ * everything from the specified start until the newest record to be deleted. No set end
+ * will default to {@link java.time.Instant#MAX}.
+ */
+ public @NonNull Builder setEnd(@NonNull Instant end) {
+ Objects.requireNonNull(end);
+ mEnd = end;
+ return this;
+ }
+
+ /** Builds a {@link DeletionRequest} instance. */
+ public @NonNull DeletionRequest build() {
+ if (mDomainUris == null) {
+ mDomainUris = new ArrayList<>();
+ }
+ if (mOriginUris == null) {
+ mOriginUris = new ArrayList<>();
+ }
+ return new DeletionRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/MeasurementErrorResponse.java b/android-34/android/adservices/measurement/MeasurementErrorResponse.java
new file mode 100644
index 0000000..204ed1f
--- /dev/null
+++ b/android-34/android/adservices/measurement/MeasurementErrorResponse.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
+
+import android.adservices.common.AdServicesResponse;
+import android.adservices.common.AdServicesStatusUtils;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represents a generic response for Measurement APIs.
+ *
+ * @hide
+ */
+public final class MeasurementErrorResponse extends AdServicesResponse {
+ @NonNull
+ public static final Creator<MeasurementErrorResponse> CREATOR =
+ new Parcelable.Creator<MeasurementErrorResponse>() {
+ @Override
+ public MeasurementErrorResponse createFromParcel(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ return new MeasurementErrorResponse(in);
+ }
+
+ @Override
+ public MeasurementErrorResponse[] newArray(int size) {
+ return new MeasurementErrorResponse[size];
+ }
+ };
+
+ protected MeasurementErrorResponse(@NonNull Builder builder) {
+ super(builder.mStatusCode, builder.mErrorMessage);
+ }
+
+ protected MeasurementErrorResponse(@NonNull Parcel in) {
+ super(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+
+ dest.writeInt(mStatusCode);
+ dest.writeString(mErrorMessage);
+ }
+
+ /**
+ * Builder for {@link MeasurementErrorResponse} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @AdServicesStatusUtils.StatusCode private int mStatusCode = STATUS_SUCCESS;
+ @Nullable private String mErrorMessage;
+
+ public Builder() {}
+
+ /** Set the Status Code. */
+ @NonNull
+ public MeasurementErrorResponse.Builder setStatusCode(
+ @AdServicesStatusUtils.StatusCode int statusCode) {
+ mStatusCode = statusCode;
+ return this;
+ }
+
+ /** Set the Error Message. */
+ @NonNull
+ public MeasurementErrorResponse.Builder setErrorMessage(@Nullable String errorMessage) {
+ mErrorMessage = errorMessage;
+ return this;
+ }
+
+ /** Builds a {@link MeasurementErrorResponse} instance. */
+ @NonNull
+ public MeasurementErrorResponse build() {
+ return new MeasurementErrorResponse(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/MeasurementManager.java b/android-34/android/adservices/measurement/MeasurementManager.java
new file mode 100644
index 0000000..45e133a
--- /dev/null
+++ b/android-34/android/adservices/measurement/MeasurementManager.java
@@ -0,0 +1,702 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.measurement;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION;
+import static android.adservices.common.AdServicesStatusUtils.ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE;
+
+import android.adservices.adid.AdId;
+import android.adservices.adid.AdIdManager;
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.CallerMetadata;
+import android.adservices.common.SandboxedSdkContextUtils;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.app.sdksandbox.SandboxedSdkContext;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.view.InputEvent;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.LogUtil;
+import com.android.adservices.ServiceBinder;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** MeasurementManager provides APIs to manage source and trigger registrations. */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public class MeasurementManager {
+ /** @hide */
+ public static final String MEASUREMENT_SERVICE = "measurement_service";
+
+ /**
+ * This state indicates that Measurement APIs are unavailable. Invoking them will result in an
+ * {@link UnsupportedOperationException}.
+ */
+ public static final int MEASUREMENT_API_STATE_DISABLED = 0;
+
+ /**
+ * This state indicates that Measurement APIs are enabled.
+ */
+ public static final int MEASUREMENT_API_STATE_ENABLED = 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = "MEASUREMENT_API_STATE_",
+ value = {
+ MEASUREMENT_API_STATE_DISABLED,
+ MEASUREMENT_API_STATE_ENABLED,
+ })
+ public @interface MeasurementApiState {}
+
+ private interface MeasurementAdIdCallback {
+ void onAdIdCallback(boolean isAdIdEnabled, @Nullable String adIdValue);
+ }
+
+ private final long AD_ID_TIMEOUT_MS = 400;
+
+ private Context mContext;
+ private ServiceBinder<IMeasurementService> mServiceBinder;
+ private AdIdManager mAdIdManager;
+ private Executor mAdIdExecutor = Executors.newCachedThreadPool();
+
+ private static final String DEBUG_API_WARNING_MESSAGE =
+ "To enable debug api, include ACCESS_ADSERVICES_AD_ID "
+ + "permission and enable advertising ID under device settings";
+
+ /**
+ * Factory method for creating an instance of MeasurementManager.
+ *
+ * @param context The {@link Context} to use
+ * @return A {@link MeasurementManager} instance
+ */
+ @NonNull
+ public static MeasurementManager get(@NonNull Context context) {
+ // On T+, context.getSystemService() does more than just call constructor.
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ ? context.getSystemService(MeasurementManager.class)
+ : new MeasurementManager(context);
+ }
+
+ /**
+ * This is for test purposes, it helps to mock the adIdManager.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public static MeasurementManager get(
+ @NonNull Context context, @NonNull AdIdManager adIdManager) {
+ MeasurementManager measurementManager = MeasurementManager.get(context);
+ measurementManager.mAdIdManager = adIdManager;
+ return measurementManager;
+ }
+
+ /**
+ * Create MeasurementManager.
+ *
+ * @hide
+ */
+ public MeasurementManager(Context context) {
+ // TODO(b/269798827): Enable for R.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ throw new IllegalStateException(ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE);
+ }
+
+ // In case the MeasurementManager is initiated from inside a sdk_sandbox process the
+ // fields will be immediately rewritten by the initialize method below.
+ initialize(context);
+ }
+
+ /**
+ * Initializes {@link MeasurementManager} with the given {@code context}.
+ *
+ * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context.
+ * For more information check the javadoc on the {@link
+ * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}.
+ *
+ * @hide
+ * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry
+ */
+ public MeasurementManager initialize(@NonNull Context context) {
+ mContext = context;
+ mServiceBinder = ServiceBinder.getServiceBinder(
+ context,
+ AdServicesCommon.ACTION_MEASUREMENT_SERVICE,
+ IMeasurementService.Stub::asInterface);
+ mAdIdManager = AdIdManager.get(context);
+ return this;
+ }
+
+ /**
+ * Retrieves an {@link IMeasurementService} implementation
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public IMeasurementService getService() throws IllegalStateException {
+ IMeasurementService service = mServiceBinder.getService();
+ if (service == null) {
+ throw new IllegalStateException("Unable to find the service");
+ }
+ return service;
+ }
+
+ /** Checks if Ad ID permission is enabled. */
+ private boolean isAdIdPermissionEnabled(AdId adId) {
+ return !AdId.ZERO_OUT.equals(adId.getAdId());
+ }
+
+ /**
+ * Register an attribution source / trigger.
+ *
+ * @hide
+ */
+ private void register(
+ @NonNull RegistrationRequest registrationRequest,
+ @NonNull IMeasurementService service,
+ @Nullable @CallbackExecutor Executor executor,
+ @Nullable OutcomeReceiver<Object, Exception> callback) {
+ Objects.requireNonNull(registrationRequest);
+
+ String registrationType = "source";
+ if (registrationRequest.getRegistrationType() == RegistrationRequest.REGISTER_TRIGGER) {
+ registrationType = "trigger";
+ }
+ LogUtil.d("Registering " + registrationType);
+
+ try {
+ service.register(
+ registrationRequest,
+ generateCallerMetadataWithCurrentTime(),
+ new IMeasurementCallback.Stub() {
+ @Override
+ public void onResult() {
+ if (callback != null && executor != null) {
+ executor.execute(() -> callback.onResult(new Object()));
+ }
+ }
+
+ @Override
+ public void onFailure(MeasurementErrorResponse failureParcel) {
+ if (callback != null && executor != null) {
+ executor.execute(
+ () ->
+ callback.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ LogUtil.e(e, "RemoteException");
+ if (callback != null && executor != null) {
+ executor.execute(() -> callback.onError(new IllegalStateException(e)));
+ }
+ }
+ }
+
+ /**
+ * Register an attribution source (click or view).
+ *
+ * @param attributionSource the platform issues a request to this URI in order to fetch metadata
+ * associated with the attribution source. The source metadata is stored on device, making
+ * it eligible to be matched to future triggers.
+ * @param inputEvent either an {@link InputEvent} object (for a click event) or null (for a view
+ * event).
+ * @param executor used by callback to dispatch results.
+ * @param callback intended to notify asynchronously the API result.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+ public void registerSource(
+ @NonNull Uri attributionSource,
+ @Nullable InputEvent inputEvent,
+ @Nullable @CallbackExecutor Executor executor,
+ @Nullable OutcomeReceiver<Object, Exception> callback) {
+ Objects.requireNonNull(attributionSource);
+
+ IMeasurementService service = getServiceWrapper(executor, callback);
+
+ if (service == null) {
+ LogUtil.d("Measurement service not found");
+ return;
+ }
+
+ final RegistrationRequest.Builder builder =
+ new RegistrationRequest.Builder(
+ RegistrationRequest.REGISTER_SOURCE,
+ attributionSource,
+ getAppPackageName(),
+ getSdkPackageName())
+ .setRequestTime(SystemClock.uptimeMillis())
+ .setInputEvent(inputEvent);
+ // TODO(b/281546062): Can probably remove isAdIdEnabled, since whether adIdValue is null or
+ // not will determine if adId is enabled.
+ getAdId(
+ (isAdIdEnabled, adIdValue) ->
+ register(
+ builder.setAdIdPermissionGranted(isAdIdEnabled)
+ .setAdIdValue(adIdValue)
+ .build(),
+ service,
+ executor,
+ callback));
+ }
+
+ /**
+ * Register an attribution source(click or view) from web context. This API will not process any
+ * redirects, all registration URLs should be supplied with the request. At least one of
+ * appDestination or webDestination parameters are required to be provided. If the registration
+ * is successful, {@code callback}'s {@link OutcomeReceiver#onResult} is invoked with null. In
+ * case of failure, a {@link Exception} is sent through {@code callback}'s {@link
+ * OutcomeReceiver#onError}. Both success and failure feedback are executed on the provided
+ * {@link Executor}.
+ *
+ * @param request source registration request
+ * @param executor used by callback to dispatch results.
+ * @param callback intended to notify asynchronously the API result.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+ public void registerWebSource(
+ @NonNull WebSourceRegistrationRequest request,
+ @Nullable Executor executor,
+ @Nullable OutcomeReceiver<Object, Exception> callback) {
+ Objects.requireNonNull(request);
+
+ IMeasurementService service = getServiceWrapper(executor, callback);
+
+ if (service == null) {
+ LogUtil.d("Measurement service not found");
+ return;
+ }
+
+ CallerMetadata callerMetadata = generateCallerMetadataWithCurrentTime();
+ IMeasurementCallback measurementCallback =
+ new IMeasurementCallback.Stub() {
+ @Override
+ public void onResult() {
+ if (callback != null && executor != null) {
+ executor.execute(() -> callback.onResult(new Object()));
+ }
+ }
+
+ @Override
+ public void onFailure(MeasurementErrorResponse failureParcel) {
+ if (callback != null && executor != null) {
+ executor.execute(
+ () ->
+ callback.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ }
+ };
+
+ final WebSourceRegistrationRequestInternal.Builder builder =
+ new WebSourceRegistrationRequestInternal.Builder(
+ request,
+ getAppPackageName(),
+ getSdkPackageName(),
+ SystemClock.uptimeMillis());
+
+ getAdId(
+ (isAdIdEnabled, adIdValue) ->
+ registerWebSourceWrapper(
+ builder.setAdIdPermissionGranted(isAdIdEnabled).build(),
+ service,
+ executor,
+ callerMetadata,
+ measurementCallback,
+ callback));
+ }
+
+ /** Wrapper method for registerWebSource. */
+ private void registerWebSourceWrapper(
+ @NonNull WebSourceRegistrationRequestInternal request,
+ @NonNull IMeasurementService service,
+ @Nullable Executor executor,
+ @NonNull CallerMetadata callerMetadata,
+ @NonNull IMeasurementCallback measurementCallback,
+ @Nullable OutcomeReceiver<Object, Exception> callback) {
+ try {
+ LogUtil.d("Registering web source");
+ service.registerWebSource(request, callerMetadata, measurementCallback);
+ } catch (RemoteException e) {
+ LogUtil.e(e, "RemoteException");
+ if (callback != null && executor != null) {
+ executor.execute(() -> callback.onError(new IllegalStateException(e)));
+ }
+ }
+ }
+
+ /**
+ * Register an attribution trigger(click or view) from web context. This API will not process
+ * any redirects, all registration URLs should be supplied with the request. If the registration
+ * is successful, {@code callback}'s {@link OutcomeReceiver#onResult} is invoked with null. In
+ * case of failure, a {@link Exception} is sent through {@code callback}'s {@link
+ * OutcomeReceiver#onError}. Both success and failure feedback are executed on the provided
+ * {@link Executor}.
+ *
+ * @param request trigger registration request
+ * @param executor used by callback to dispatch results
+ * @param callback intended to notify asynchronously the API result
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+ public void registerWebTrigger(
+ @NonNull WebTriggerRegistrationRequest request,
+ @Nullable Executor executor,
+ @Nullable OutcomeReceiver<Object, Exception> callback) {
+ Objects.requireNonNull(request);
+
+ IMeasurementService service = getServiceWrapper(executor, callback);
+
+ if (service == null) {
+ LogUtil.d("Measurement service not found");
+ return;
+ }
+
+ CallerMetadata callerMetadata = generateCallerMetadataWithCurrentTime();
+ IMeasurementCallback measurementCallback =
+ new IMeasurementCallback.Stub() {
+ @Override
+ public void onResult() {
+ if (callback != null && executor != null) {
+ executor.execute(() -> callback.onResult(new Object()));
+ }
+ }
+
+ @Override
+ public void onFailure(MeasurementErrorResponse failureParcel) {
+ if (callback != null && executor != null) {
+ executor.execute(
+ () ->
+ callback.onError(
+ AdServicesStatusUtils.asException(
+ failureParcel)));
+ }
+ }
+ };
+
+ WebTriggerRegistrationRequestInternal.Builder builder =
+ new WebTriggerRegistrationRequestInternal.Builder(
+ request, getAppPackageName(), getSdkPackageName());
+
+ getAdId(
+ (isAdIdEnabled, adIdValue) ->
+ registerWebTriggerWrapper(
+ builder.setAdIdPermissionGranted(isAdIdEnabled).build(),
+ service,
+ executor,
+ callerMetadata,
+ measurementCallback,
+ callback));
+ }
+
+ /** Wrapper method for registerWebTrigger. */
+ private void registerWebTriggerWrapper(
+ @NonNull WebTriggerRegistrationRequestInternal request,
+ @NonNull IMeasurementService service,
+ @Nullable Executor executor,
+ @NonNull CallerMetadata callerMetadata,
+ @NonNull IMeasurementCallback measurementCallback,
+ @Nullable OutcomeReceiver<Object, Exception> callback) {
+ try {
+ LogUtil.d("Registering web trigger");
+ service.registerWebTrigger(request, callerMetadata, measurementCallback);
+ } catch (RemoteException e) {
+ LogUtil.e(e, "RemoteException");
+ if (callback != null && executor != null) {
+ executor.execute(() -> callback.onError(new IllegalStateException(e)));
+ }
+ }
+ }
+
+ /**
+ * Register a trigger (conversion).
+ *
+ * @param trigger the API issues a request to this URI to fetch metadata associated with the
+ * trigger. The trigger metadata is stored on-device, and is eligible to be matched with
+ * sources during the attribution process.
+ * @param executor used by callback to dispatch results.
+ * @param callback intended to notify asynchronously the API result.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+ public void registerTrigger(
+ @NonNull Uri trigger,
+ @Nullable @CallbackExecutor Executor executor,
+ @Nullable OutcomeReceiver<Object, Exception> callback) {
+ Objects.requireNonNull(trigger);
+
+ IMeasurementService service = getServiceWrapper(executor, callback);
+
+ if (service == null) {
+ LogUtil.d("Measurement service not found");
+ return;
+ }
+
+ final RegistrationRequest.Builder builder =
+ new RegistrationRequest.Builder(
+ RegistrationRequest.REGISTER_TRIGGER,
+ trigger,
+ getAppPackageName(),
+ getSdkPackageName());
+ // TODO(b/281546062)
+ getAdId(
+ (isAdIdEnabled, adIdValue) ->
+ register(
+ builder.setAdIdPermissionGranted(isAdIdEnabled)
+ .setAdIdValue(adIdValue)
+ .build(),
+ service,
+ executor,
+ callback));
+ }
+
+ /**
+ * Delete previously registered data.
+ *
+ * @hide
+ */
+ private void deleteRegistrations(
+ @NonNull DeletionParam deletionParam,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> callback) {
+ Objects.requireNonNull(deletionParam);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ final IMeasurementService service = getServiceWrapper(executor, callback);
+
+ if (service == null) {
+ LogUtil.d("Measurement service not found");
+ return;
+ }
+
+ try {
+ service.deleteRegistrations(
+ deletionParam,
+ generateCallerMetadataWithCurrentTime(),
+ new IMeasurementCallback.Stub() {
+ @Override
+ public void onResult() {
+ executor.execute(() -> callback.onResult(new Object()));
+ }
+
+ @Override
+ public void onFailure(MeasurementErrorResponse failureParcel) {
+ executor.execute(
+ () -> {
+ callback.onError(
+ AdServicesStatusUtils.asException(failureParcel));
+ });
+ }
+ });
+ } catch (RemoteException e) {
+ LogUtil.e(e, "RemoteException");
+ executor.execute(() -> callback.onError(new IllegalStateException(e)));
+ }
+ }
+
+ /**
+ * Delete previous registrations. If the deletion is successful, the callback's {@link
+ * OutcomeReceiver#onResult} is invoked with null. In case of failure, a {@link Exception} is
+ * sent through the callback's {@link OutcomeReceiver#onError}. Both success and failure
+ * feedback are executed on the provided {@link Executor}.
+ *
+ * @param deletionRequest The request for deleting data.
+ * @param executor The executor to run callback.
+ * @param callback intended to notify asynchronously the API result.
+ */
+ public void deleteRegistrations(
+ @NonNull DeletionRequest deletionRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Object, Exception> callback) {
+ deleteRegistrations(
+ new DeletionParam.Builder(
+ deletionRequest.getOriginUris(),
+ deletionRequest.getDomainUris(),
+ deletionRequest.getStart(),
+ deletionRequest.getEnd(),
+ getAppPackageName(),
+ getSdkPackageName())
+ .setDeletionMode(deletionRequest.getDeletionMode())
+ .setMatchBehavior(deletionRequest.getMatchBehavior())
+ .build(),
+ executor,
+ callback);
+ }
+
+ /**
+ * Get Measurement API status.
+ *
+ * <p>The callback's {@code Integer} value is one of {@code MeasurementApiState}.
+ *
+ * @param executor used by callback to dispatch results.
+ * @param callback intended to notify asynchronously the API result.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+ public void getMeasurementApiStatus(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Integer, Exception> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ final IMeasurementService service;
+ try {
+ service = getService();
+ } catch (IllegalStateException e) {
+ LogUtil.e(e, "Failed to bind to measurement service");
+ executor.execute(() -> callback.onResult(MEASUREMENT_API_STATE_DISABLED));
+ return;
+ } catch (RuntimeException e) {
+ LogUtil.e(e, "Unknown failure while binding measurement service");
+ executor.execute(() -> callback.onError(e));
+ return;
+ }
+
+ try {
+ service.getMeasurementApiStatus(
+ new StatusParam.Builder(getAppPackageName(), getSdkPackageName()).build(),
+ generateCallerMetadataWithCurrentTime(),
+ new IMeasurementApiStatusCallback.Stub() {
+ @Override
+ public void onResult(int result) {
+ executor.execute(() -> callback.onResult(result));
+ }
+ });
+ } catch (RemoteException e) {
+ LogUtil.e(e, "RemoteException");
+ executor.execute(() -> callback.onResult(MEASUREMENT_API_STATE_DISABLED));
+ } catch (RuntimeException e) {
+ LogUtil.e(e, "Unknown failure while getting measurement status");
+ executor.execute(() -> callback.onError(e));
+ }
+ }
+
+ /**
+ * If the service is in an APK (as opposed to the system service), unbind it from the service to
+ * allow the APK process to die.
+ *
+ * @hide Not sure if we'll need this functionality in the final API. For now, we need it for
+ * performance testing to simulate "cold-start" situations.
+ */
+ @VisibleForTesting
+ public void unbindFromService() {
+ mServiceBinder.unbindFromService();
+ }
+
+ /** Returns the package name of the app from the SDK or app context */
+ private String getAppPackageName() {
+ SandboxedSdkContext sandboxedSdkContext =
+ SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
+ return sandboxedSdkContext == null
+ ? mContext.getPackageName()
+ : sandboxedSdkContext.getClientPackageName();
+ }
+
+ /** Returns the package name of the sdk from the SDK or empty if no SDK found */
+ private String getSdkPackageName() {
+ SandboxedSdkContext sandboxedSdkContext =
+ SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
+ return sandboxedSdkContext == null ? "" : sandboxedSdkContext.getSdkPackageName();
+ }
+
+ private CallerMetadata generateCallerMetadataWithCurrentTime() {
+ return new CallerMetadata.Builder()
+ .setBinderElapsedTimestamp(SystemClock.elapsedRealtime())
+ .build();
+ }
+
+ /** Get Service wrapper, propagates error to the caller */
+ @Nullable
+ private IMeasurementService getServiceWrapper(
+ @Nullable @CallbackExecutor Executor executor,
+ @Nullable OutcomeReceiver<Object, Exception> callback) {
+ IMeasurementService service = null;
+ try {
+ service = getService();
+ } catch (RuntimeException e) {
+ LogUtil.e(e, "Failed binding to measurement service");
+ if (callback != null && executor != null) {
+ executor.execute(() -> callback.onError(e));
+ }
+ }
+ return service;
+ }
+
+ /* Make AdId call with timeout */
+ private void getAdId(MeasurementAdIdCallback measurementAdIdCallback) {
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ AtomicBoolean isAdIdEnabled = new AtomicBoolean();
+ AtomicReference<String> adIdValue = new AtomicReference<>();
+ mAdIdManager.getAdId(
+ mAdIdExecutor,
+ new OutcomeReceiver<AdId, Exception>() {
+ @Override
+ public void onResult(AdId adId) {
+ isAdIdEnabled.set(isAdIdPermissionEnabled(adId));
+ adIdValue.set(adId.getAdId().equals(AdId.ZERO_OUT) ? null : adId.getAdId());
+ LogUtil.d("AdId permission enabled %b", isAdIdEnabled.get());
+ countDownLatch.countDown();
+ }
+
+ @Override
+ public void onError(Exception error) {
+ boolean isExpected =
+ error instanceof IllegalStateException
+ || error instanceof SecurityException;
+ if (isExpected) {
+ LogUtil.w(DEBUG_API_WARNING_MESSAGE);
+ } else {
+ LogUtil.w(error, DEBUG_API_WARNING_MESSAGE);
+ }
+
+ countDownLatch.countDown();
+ }
+ });
+
+ boolean timedOut = false;
+ try {
+ timedOut = !countDownLatch.await(AD_ID_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ LogUtil.w(e, "InterruptedException while waiting for AdId");
+ }
+ if (timedOut) {
+ LogUtil.w("AdId call timed out");
+ }
+ measurementAdIdCallback.onAdIdCallback(isAdIdEnabled.get(), adIdValue.get());
+ }
+}
diff --git a/android-34/android/adservices/measurement/RegistrationRequest.java b/android-34/android/adservices/measurement/RegistrationRequest.java
new file mode 100644
index 0000000..e44869c
--- /dev/null
+++ b/android-34/android/adservices/measurement/RegistrationRequest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.measurement;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.InputEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+
+/**
+ * Class to hold input to measurement registration calls.
+ * @hide
+ */
+public final class RegistrationRequest implements Parcelable {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ INVALID,
+ REGISTER_SOURCE,
+ REGISTER_TRIGGER,
+ })
+ public @interface RegistrationType {}
+ /** Invalid registration type used as a default. */
+ public static final int INVALID = 0;
+ /**
+ * A request to register an Attribution Source event (NOTE: AdServices type not
+ * android.context.AttributionSource).
+ */
+ public static final int REGISTER_SOURCE = 1;
+ /** A request to register a trigger event. */
+ public static final int REGISTER_TRIGGER = 2;
+
+ @RegistrationType private final int mRegistrationType;
+ private final Uri mRegistrationUri;
+ private final InputEvent mInputEvent;
+ private final String mAppPackageName;
+ private final String mSdkPackageName;
+ private final long mRequestTime;
+ private final boolean mIsAdIdPermissionGranted;
+ private final String mAdIdValue;
+
+ private RegistrationRequest(@NonNull Builder builder) {
+ mRegistrationType = builder.mRegistrationType;
+ mRegistrationUri = builder.mRegistrationUri;
+ mInputEvent = builder.mInputEvent;
+ mAppPackageName = builder.mAppPackageName;
+ mSdkPackageName = builder.mSdkPackageName;
+ mRequestTime = builder.mRequestTime;
+ mIsAdIdPermissionGranted = builder.mIsAdIdPermissionGranted;
+ mAdIdValue = builder.mAdIdValue;
+ }
+
+ /**
+ * Unpack an RegistrationRequest from a Parcel.
+ */
+ private RegistrationRequest(Parcel in) {
+ mRegistrationType = in.readInt();
+ mRegistrationUri = Uri.CREATOR.createFromParcel(in);
+ mAppPackageName = in.readString();
+ mSdkPackageName = in.readString();
+ boolean hasInputEvent = in.readBoolean();
+ if (hasInputEvent) {
+ mInputEvent = InputEvent.CREATOR.createFromParcel(in);
+ } else {
+ mInputEvent = null;
+ }
+ mRequestTime = in.readLong();
+ mIsAdIdPermissionGranted = in.readBoolean();
+ boolean hasAdIdValue = in.readBoolean();
+ if (hasAdIdValue) {
+ mAdIdValue = in.readString();
+ } else {
+ mAdIdValue = null;
+ }
+ }
+
+ /** Creator for Parcelable (via reflection). */
+ @NonNull
+ public static final Parcelable.Creator<RegistrationRequest> CREATOR =
+ new Parcelable.Creator<RegistrationRequest>() {
+ @Override
+ public RegistrationRequest createFromParcel(Parcel in) {
+ return new RegistrationRequest(in);
+ }
+
+ @Override
+ public RegistrationRequest[] newArray(int size) {
+ return new RegistrationRequest[size];
+ }
+ };
+
+ /**
+ * For Parcelable, no special marshalled objects.
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * For Parcelable, write out to a Parcel in particular order.
+ */
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ out.writeInt(mRegistrationType);
+ mRegistrationUri.writeToParcel(out, flags);
+ out.writeString(mAppPackageName);
+ out.writeString(mSdkPackageName);
+ if (mInputEvent != null) {
+ out.writeBoolean(true);
+ mInputEvent.writeToParcel(out, flags);
+ } else {
+ out.writeBoolean(false);
+ }
+ out.writeLong(mRequestTime);
+ out.writeBoolean(mIsAdIdPermissionGranted);
+ if (mAdIdValue != null) {
+ out.writeBoolean(true);
+ out.writeString(mAdIdValue);
+ } else {
+ out.writeBoolean(false);
+ }
+ }
+
+ /** Type of the registration. */
+ @RegistrationType
+ public int getRegistrationType() {
+ return mRegistrationType;
+ }
+
+ /** Source URI of the App / Publisher. */
+ @NonNull
+ public Uri getRegistrationUri() {
+ return mRegistrationUri;
+ }
+
+ /** InputEvent related to an ad event. */
+ @Nullable
+ public InputEvent getInputEvent() {
+ return mInputEvent;
+ }
+
+ /** Package name of the app used for the registration. */
+ @NonNull
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /** Package name of the sdk used for the registration. */
+ @NonNull
+ public String getSdkPackageName() {
+ return mSdkPackageName;
+ }
+
+ /** Time the request was created, as millis since boot excluding time in deep sleep. */
+ @NonNull
+ public long getRequestTime() {
+ return mRequestTime;
+ }
+
+ /** Ad ID Permission */
+ @NonNull
+ public boolean isAdIdPermissionGranted() {
+ return mIsAdIdPermissionGranted;
+ }
+
+ /** Ad ID Value */
+ @Nullable
+ public String getAdIdValue() {
+ return mAdIdValue;
+ }
+
+ /**
+ * A builder for {@link RegistrationRequest}.
+ */
+ public static final class Builder {
+ @RegistrationType private final int mRegistrationType;
+ private final Uri mRegistrationUri;
+ private final String mAppPackageName;
+ private final String mSdkPackageName;
+ private InputEvent mInputEvent;
+ private long mRequestTime;
+ private boolean mIsAdIdPermissionGranted;
+ private String mAdIdValue;
+
+ /**
+ * Builder constructor for {@link RegistrationRequest}.
+ *
+ * @param type registration type, either source or trigger
+ * @param registrationUri registration uri endpoint for registering a source/trigger
+ * @param appPackageName app package name that is calling PP API
+ * @param sdkPackageName sdk package name that is calling PP API
+ */
+ public Builder(
+ @RegistrationType int type,
+ @NonNull Uri registrationUri,
+ @NonNull String appPackageName,
+ @NonNull String sdkPackageName) {
+ if (type != REGISTER_SOURCE && type != REGISTER_TRIGGER) {
+ throw new IllegalArgumentException("Invalid registrationType");
+ }
+
+ Objects.requireNonNull(registrationUri);
+ Objects.requireNonNull(appPackageName);
+ Objects.requireNonNull(sdkPackageName);
+ mRegistrationType = type;
+ mRegistrationUri = registrationUri;
+ mAppPackageName = appPackageName;
+ mSdkPackageName = sdkPackageName;
+ }
+
+ /** See {@link RegistrationRequest#getInputEvent}. */
+ @NonNull
+ public Builder setInputEvent(@Nullable InputEvent event) {
+ mInputEvent = event;
+ return this;
+ }
+
+ /** See {@link RegistrationRequest#getRequestTime}. */
+ @NonNull
+ public Builder setRequestTime(long requestTime) {
+ mRequestTime = requestTime;
+ return this;
+ }
+
+ /** See {@link RegistrationRequest#isAdIdPermissionGranted()}. */
+ @NonNull
+ public Builder setAdIdPermissionGranted(boolean adIdPermissionGranted) {
+ mIsAdIdPermissionGranted = adIdPermissionGranted;
+ return this;
+ }
+
+ /** See {@link RegistrationRequest#getAdIdValue()}. */
+ @NonNull
+ public Builder setAdIdValue(@Nullable String adIdValue) {
+ mAdIdValue = adIdValue;
+ return this;
+ }
+
+ /** Build the RegistrationRequest. */
+ @NonNull
+ public RegistrationRequest build() {
+ // Ensure registrationType has been set,
+ // throws IllegalArgumentException if mRegistrationType
+ // isn't a valid choice.
+ if (mRegistrationType != REGISTER_SOURCE && mRegistrationType != REGISTER_TRIGGER) {
+ throw new IllegalArgumentException("Invalid registrationType");
+ }
+
+ return new RegistrationRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/StatusParam.java b/android-34/android/adservices/measurement/StatusParam.java
new file mode 100644
index 0000000..a7b3e94
--- /dev/null
+++ b/android-34/android/adservices/measurement/StatusParam.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Class to hold parameters needed for getting the Measurement API status. This is an internal class
+ * for communication between the {@link MeasurementManager} and {@link IMeasurementService} impl.
+ *
+ * @hide
+ */
+public final class StatusParam implements Parcelable {
+ private final String mAppPackageName;
+ private final String mSdkPackageName;
+
+ private StatusParam(@NonNull Builder builder) {
+ mAppPackageName = builder.mAppPackageName;
+ mSdkPackageName = builder.mSdkPackageName;
+ }
+
+ /** Unpack an StatusParam from a Parcel. */
+ private StatusParam(Parcel in) {
+ mAppPackageName = in.readString();
+ mSdkPackageName = in.readString();
+ }
+
+ /** Creator for Parcelable (via reflection). */
+ @NonNull
+ public static final Creator<StatusParam> CREATOR =
+ new Creator<StatusParam>() {
+ @Override
+ public StatusParam createFromParcel(Parcel in) {
+ return new StatusParam(in);
+ }
+
+ @Override
+ public StatusParam[] newArray(int size) {
+ return new StatusParam[size];
+ }
+ };
+
+ /** For Parcelable, no special marshalled objects. */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** For Parcelable, write out to a Parcel in particular order. */
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ out.writeString(mAppPackageName);
+ out.writeString(mSdkPackageName);
+ }
+
+ /** Package name of the app used for getting the status. */
+ @NonNull
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /** Package name of the sdk used for getting the status. */
+ @NonNull
+ public String getSdkPackageName() {
+ return mSdkPackageName;
+ }
+
+ /** A builder for {@link StatusParam}. */
+ public static final class Builder {
+ private final String mAppPackageName;
+ private final String mSdkPackageName;
+
+ /**
+ * Builder constructor for {@link StatusParam}.
+ *
+ * @param appPackageName see {@link StatusParam#getAppPackageName()}
+ * @param sdkPackageName see {@link StatusParam#getSdkPackageName()}
+ */
+ public Builder(@NonNull String appPackageName, @NonNull String sdkPackageName) {
+ Objects.requireNonNull(appPackageName);
+ Objects.requireNonNull(sdkPackageName);
+ mAppPackageName = appPackageName;
+ mSdkPackageName = sdkPackageName;
+ }
+
+ /** Build the StatusParam. */
+ @NonNull
+ public StatusParam build() {
+ return new StatusParam(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/WebSourceParams.java b/android-34/android/adservices/measurement/WebSourceParams.java
new file mode 100644
index 0000000..f675cf7
--- /dev/null
+++ b/android-34/android/adservices/measurement/WebSourceParams.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/** Class holding source registration parameters. */
+public final class WebSourceParams implements Parcelable {
+ /** Creator for Paracelable (via reflection). */
+ @NonNull
+ public static final Parcelable.Creator<WebSourceParams> CREATOR =
+ new Parcelable.Creator<WebSourceParams>() {
+ @Override
+ public WebSourceParams createFromParcel(Parcel in) {
+ return new WebSourceParams(in);
+ }
+
+ @Override
+ public WebSourceParams[] newArray(int size) {
+ return new WebSourceParams[size];
+ }
+ };
+ /**
+ * URI that the Attribution Reporting API sends a request to in order to obtain source
+ * registration parameters.
+ */
+ @NonNull private final Uri mRegistrationUri;
+ /**
+ * Used by the browser to indicate whether the debug key obtained from the registration URI is
+ * allowed to be used
+ */
+ private final boolean mDebugKeyAllowed;
+
+ private WebSourceParams(@NonNull Builder builder) {
+ mRegistrationUri = builder.mRegistrationUri;
+ mDebugKeyAllowed = builder.mDebugKeyAllowed;
+ }
+
+ /** Unpack a SourceRegistration from a Parcel. */
+ private WebSourceParams(@NonNull Parcel in) {
+ mRegistrationUri = Uri.CREATOR.createFromParcel(in);
+ mDebugKeyAllowed = in.readBoolean();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WebSourceParams)) return false;
+ WebSourceParams that = (WebSourceParams) o;
+ return mDebugKeyAllowed == that.mDebugKeyAllowed
+ && Objects.equals(mRegistrationUri, that.mRegistrationUri);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRegistrationUri, mDebugKeyAllowed);
+ }
+
+ /** Getter for registration Uri. */
+ @NonNull
+ public Uri getRegistrationUri() {
+ return mRegistrationUri;
+ }
+
+ /**
+ * Getter for debug allowed/disallowed flag. Its value as {@code true} means to allow parsing
+ * debug keys from registration responses and their addition in the generated reports.
+ */
+ public boolean isDebugKeyAllowed() {
+ return mDebugKeyAllowed;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ mRegistrationUri.writeToParcel(out, flags);
+ out.writeBoolean(mDebugKeyAllowed);
+ }
+
+ /** A builder for {@link WebSourceParams}. */
+ public static final class Builder {
+ /**
+ * URI that the Attribution Reporting API sends a request to in order to obtain source
+ * registration parameters.
+ */
+ @NonNull private final Uri mRegistrationUri;
+ /**
+ * Used by the browser to indicate whether the debug key obtained from the registration URI
+ * is allowed to be used
+ */
+ private boolean mDebugKeyAllowed;
+
+ /**
+ * Builder constructor for {@link WebSourceParams}. {@code mIsDebugKeyAllowed} is assigned
+ * false by default.
+ *
+ * @param registrationUri URI that the Attribution Reporting API sends a request to in order
+ * to obtain source registration parameters.
+ */
+ public Builder(@NonNull Uri registrationUri) {
+ Objects.requireNonNull(registrationUri);
+ mRegistrationUri = registrationUri;
+ mDebugKeyAllowed = false;
+ }
+
+ /**
+ * Setter for debug allow/disallow flag. Setting it to true will allow parsing debug keys
+ * from registration responses and their addition in the generated reports.
+ *
+ * @param debugKeyAllowed used by the browser to indicate whether the debug key obtained
+ * from the registration URI is allowed to be used
+ * @return builder
+ */
+ @NonNull
+ public Builder setDebugKeyAllowed(boolean debugKeyAllowed) {
+ this.mDebugKeyAllowed = debugKeyAllowed;
+ return this;
+ }
+
+ /**
+ * Built immutable {@link WebSourceParams}.
+ *
+ * @return immutable {@link WebSourceParams}
+ */
+ @NonNull
+ public WebSourceParams build() {
+ return new WebSourceParams(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/WebSourceRegistrationRequest.java b/android-34/android/adservices/measurement/WebSourceRegistrationRequest.java
new file mode 100644
index 0000000..62d286c
--- /dev/null
+++ b/android-34/android/adservices/measurement/WebSourceRegistrationRequest.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.InputEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** Class to hold input to measurement source registration calls from web context. */
+public final class WebSourceRegistrationRequest implements Parcelable {
+ private static final String ANDROID_APP_SCHEME = "android-app";
+ private static final int WEB_SOURCE_PARAMS_MAX_COUNT = 20;
+
+ /** Creator for Paracelable (via reflection). */
+ @NonNull
+ public static final Parcelable.Creator<WebSourceRegistrationRequest> CREATOR =
+ new Parcelable.Creator<WebSourceRegistrationRequest>() {
+ @Override
+ public WebSourceRegistrationRequest createFromParcel(Parcel in) {
+ return new WebSourceRegistrationRequest(in);
+ }
+
+ @Override
+ public WebSourceRegistrationRequest[] newArray(int size) {
+ return new WebSourceRegistrationRequest[size];
+ }
+ };
+ /** Registration info to fetch sources. */
+ @NonNull private final List<WebSourceParams> mWebSourceParams;
+
+ /** Top level origin of publisher. */
+ @NonNull private final Uri mTopOriginUri;
+
+ /**
+ * User Interaction {@link InputEvent} used by the AttributionReporting API to distinguish
+ * clicks from views.
+ */
+ @Nullable private final InputEvent mInputEvent;
+
+ /**
+ * App destination of the source. It is the android app {@link Uri} where corresponding
+ * conversion is expected. At least one of app destination or web destination is required.
+ */
+ @Nullable private final Uri mAppDestination;
+
+ /**
+ * Web destination of the source. It is the website {@link Uri} where corresponding conversion
+ * is expected. At least one of app destination or web destination is required.
+ */
+ @Nullable private final Uri mWebDestination;
+
+ /** Verified destination by the caller. This is where the user actually landed. */
+ @Nullable private final Uri mVerifiedDestination;
+
+ private WebSourceRegistrationRequest(@NonNull Builder builder) {
+ mWebSourceParams = builder.mWebSourceParams;
+ mInputEvent = builder.mInputEvent;
+ mTopOriginUri = builder.mTopOriginUri;
+ mAppDestination = builder.mAppDestination;
+ mWebDestination = builder.mWebDestination;
+ mVerifiedDestination = builder.mVerifiedDestination;
+ }
+
+ private WebSourceRegistrationRequest(@NonNull Parcel in) {
+ Objects.requireNonNull(in);
+ ArrayList<WebSourceParams> sourceRegistrations = new ArrayList<>();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ in.readList(sourceRegistrations, WebSourceParams.class.getClassLoader());
+ } else {
+ in.readList(
+ sourceRegistrations,
+ WebSourceParams.class.getClassLoader(),
+ WebSourceParams.class);
+ }
+ mWebSourceParams = sourceRegistrations;
+ mTopOriginUri = Uri.CREATOR.createFromParcel(in);
+ if (in.readBoolean()) {
+ mInputEvent = InputEvent.CREATOR.createFromParcel(in);
+ } else {
+ mInputEvent = null;
+ }
+ if (in.readBoolean()) {
+ mAppDestination = Uri.CREATOR.createFromParcel(in);
+ } else {
+ mAppDestination = null;
+ }
+ if (in.readBoolean()) {
+ mWebDestination = Uri.CREATOR.createFromParcel(in);
+ } else {
+ mWebDestination = null;
+ }
+ if (in.readBoolean()) {
+ mVerifiedDestination = Uri.CREATOR.createFromParcel(in);
+ } else {
+ mVerifiedDestination = null;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WebSourceRegistrationRequest)) return false;
+ WebSourceRegistrationRequest that = (WebSourceRegistrationRequest) o;
+ return Objects.equals(mWebSourceParams, that.mWebSourceParams)
+ && Objects.equals(mTopOriginUri, that.mTopOriginUri)
+ && Objects.equals(mInputEvent, that.mInputEvent)
+ && Objects.equals(mAppDestination, that.mAppDestination)
+ && Objects.equals(mWebDestination, that.mWebDestination)
+ && Objects.equals(mVerifiedDestination, that.mVerifiedDestination);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mWebSourceParams,
+ mTopOriginUri,
+ mInputEvent,
+ mAppDestination,
+ mWebDestination,
+ mVerifiedDestination);
+ }
+
+ /** Getter for source params. */
+ @NonNull
+ public List<WebSourceParams> getSourceParams() {
+ return mWebSourceParams;
+ }
+
+ /** Getter for top origin Uri. */
+ @NonNull
+ public Uri getTopOriginUri() {
+ return mTopOriginUri;
+ }
+
+ /** Getter for input event. */
+ @Nullable
+ public InputEvent getInputEvent() {
+ return mInputEvent;
+ }
+
+ /**
+ * Getter for the app destination. It is the android app {@link Uri} where corresponding
+ * conversion is expected. At least one of app destination or web destination is required.
+ */
+ @Nullable
+ public Uri getAppDestination() {
+ return mAppDestination;
+ }
+
+ /**
+ * Getter for web destination. It is the website {@link Uri} where corresponding conversion is
+ * expected. At least one of app destination or web destination is required.
+ */
+ @Nullable
+ public Uri getWebDestination() {
+ return mWebDestination;
+ }
+
+ /** Getter for verified destination. */
+ @Nullable
+ public Uri getVerifiedDestination() {
+ return mVerifiedDestination;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ out.writeList(mWebSourceParams);
+ mTopOriginUri.writeToParcel(out, flags);
+
+ if (mInputEvent != null) {
+ out.writeBoolean(true);
+ mInputEvent.writeToParcel(out, flags);
+ } else {
+ out.writeBoolean(false);
+ }
+ if (mAppDestination != null) {
+ out.writeBoolean(true);
+ mAppDestination.writeToParcel(out, flags);
+ } else {
+ out.writeBoolean(false);
+ }
+ if (mWebDestination != null) {
+ out.writeBoolean(true);
+ mWebDestination.writeToParcel(out, flags);
+ } else {
+ out.writeBoolean(false);
+ }
+ if (mVerifiedDestination != null) {
+ out.writeBoolean(true);
+ mVerifiedDestination.writeToParcel(out, flags);
+ } else {
+ out.writeBoolean(false);
+ }
+ }
+
+ /** Builder for {@link WebSourceRegistrationRequest}. */
+ public static final class Builder {
+ /** Registration info to fetch sources. */
+ @NonNull private final List<WebSourceParams> mWebSourceParams;
+ /** Top origin {@link Uri} of publisher. */
+ @NonNull private final Uri mTopOriginUri;
+ /**
+ * User Interaction InputEvent used by the AttributionReporting API to distinguish clicks
+ * from views.
+ */
+ @Nullable private InputEvent mInputEvent;
+ /**
+ * App destination of the source. It is the android app {@link Uri} where corresponding
+ * conversion is expected.
+ */
+ @Nullable private Uri mAppDestination;
+ /**
+ * Web destination of the source. It is the website {@link Uri} where corresponding
+ * conversion is expected.
+ */
+ @Nullable private Uri mWebDestination;
+ /**
+ * Verified destination by the caller. If available, sources should be checked against it.
+ */
+ @Nullable private Uri mVerifiedDestination;
+
+ /**
+ * Builder constructor for {@link WebSourceRegistrationRequest}.
+ *
+ * @param webSourceParams source parameters containing source registration parameters, the
+ * list should not be empty
+ * @param topOriginUri source publisher {@link Uri}
+ */
+ public Builder(@NonNull List<WebSourceParams> webSourceParams, @NonNull Uri topOriginUri) {
+ Objects.requireNonNull(webSourceParams);
+ Objects.requireNonNull(topOriginUri);
+ if (webSourceParams.isEmpty() || webSourceParams.size() > WEB_SOURCE_PARAMS_MAX_COUNT) {
+ throw new IllegalArgumentException(
+ "web source params size is not within bounds, size: "
+ + webSourceParams.size());
+ }
+ mWebSourceParams = webSourceParams;
+ mTopOriginUri = topOriginUri;
+ }
+
+ /**
+ * Setter for input event.
+ *
+ * @param inputEvent User Interaction InputEvent used by the AttributionReporting API to
+ * distinguish clicks from views.
+ * @return builder
+ */
+ @NonNull
+ public Builder setInputEvent(@Nullable InputEvent inputEvent) {
+ mInputEvent = inputEvent;
+ return this;
+ }
+
+ /**
+ * Setter for app destination. It is the android app {@link Uri} where corresponding
+ * conversion is expected. At least one of app destination or web destination is required.
+ *
+ * @param appDestination app destination {@link Uri}
+ * @return builder
+ */
+ @NonNull
+ public Builder setAppDestination(@Nullable Uri appDestination) {
+ if (appDestination != null) {
+ String scheme = appDestination.getScheme();
+ Uri destination;
+ if (scheme == null) {
+ destination = Uri.parse(ANDROID_APP_SCHEME + "://" + appDestination);
+ } else if (!scheme.equals(ANDROID_APP_SCHEME)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "appDestination scheme must be %s " + "or null. Received: %s",
+ ANDROID_APP_SCHEME, scheme));
+ } else {
+ destination = appDestination;
+ }
+ mAppDestination = destination;
+ }
+ return this;
+ }
+
+ /**
+ * Setter for web destination. It is the website {@link Uri} where corresponding conversion
+ * is expected. At least one of app destination or web destination is required.
+ *
+ * @param webDestination web destination {@link Uri}
+ * @return builder
+ */
+ @NonNull
+ public Builder setWebDestination(@Nullable Uri webDestination) {
+ if (webDestination != null) {
+ validateScheme("Web destination", webDestination);
+ mWebDestination = webDestination;
+ }
+ return this;
+ }
+
+ /**
+ * Setter for verified destination.
+ *
+ * @param verifiedDestination verified destination
+ * @return builder
+ */
+ @NonNull
+ public Builder setVerifiedDestination(@Nullable Uri verifiedDestination) {
+ mVerifiedDestination = verifiedDestination;
+ return this;
+ }
+
+ /** Pre-validates parameters and builds {@link WebSourceRegistrationRequest}. */
+ @NonNull
+ public WebSourceRegistrationRequest build() {
+ return new WebSourceRegistrationRequest(this);
+ }
+ }
+
+ private static void validateScheme(String name, Uri uri) throws IllegalArgumentException {
+ if (uri.getScheme() == null) {
+ throw new IllegalArgumentException(name + " must have a scheme.");
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/WebSourceRegistrationRequestInternal.java b/android-34/android/adservices/measurement/WebSourceRegistrationRequestInternal.java
new file mode 100644
index 0000000..1420520
--- /dev/null
+++ b/android-34/android/adservices/measurement/WebSourceRegistrationRequestInternal.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Internal source registration request object to communicate from {@link MeasurementManager} to
+ * {@link IMeasurementService}.
+ *
+ * @hide
+ */
+public class WebSourceRegistrationRequestInternal implements Parcelable {
+ /** Creator for Parcelable (via reflection). */
+ public static final Parcelable.Creator<WebSourceRegistrationRequestInternal> CREATOR =
+ new Parcelable.Creator<WebSourceRegistrationRequestInternal>() {
+ @Override
+ public WebSourceRegistrationRequestInternal createFromParcel(Parcel in) {
+ return new WebSourceRegistrationRequestInternal(in);
+ }
+
+ @Override
+ public WebSourceRegistrationRequestInternal[] newArray(int size) {
+ return new WebSourceRegistrationRequestInternal[size];
+ }
+ };
+ /** Holds input to measurement source registration calls from web context. */
+ @NonNull private final WebSourceRegistrationRequest mSourceRegistrationRequest;
+ /** Holds app package info of where the request is coming from. */
+ @NonNull private final String mAppPackageName;
+ /** Holds sdk package info of where the request is coming from. */
+ @NonNull private final String mSdkPackageName;
+ /** Time the request was created, as millis since boot excluding time in deep sleep. */
+ private final long mRequestTime;
+ /** AD ID Permission Granted. */
+ private final boolean mIsAdIdPermissionGranted;
+
+ private WebSourceRegistrationRequestInternal(@NonNull Builder builder) {
+ mSourceRegistrationRequest = builder.mSourceRegistrationRequest;
+ mAppPackageName = builder.mAppPackageName;
+ mSdkPackageName = builder.mSdkPackageName;
+ mRequestTime = builder.mRequestTime;
+ mIsAdIdPermissionGranted = builder.mIsAdIdPermissionGranted;
+ }
+
+ private WebSourceRegistrationRequestInternal(Parcel in) {
+ Objects.requireNonNull(in);
+ mSourceRegistrationRequest = WebSourceRegistrationRequest.CREATOR.createFromParcel(in);
+ mAppPackageName = in.readString();
+ mSdkPackageName = in.readString();
+ mRequestTime = in.readLong();
+ mIsAdIdPermissionGranted = in.readBoolean();
+ }
+
+ /** Getter for {@link #mSourceRegistrationRequest}. */
+ public WebSourceRegistrationRequest getSourceRegistrationRequest() {
+ return mSourceRegistrationRequest;
+ }
+
+ /** Getter for {@link #mAppPackageName}. */
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /** Getter for {@link #mSdkPackageName}. */
+ public String getSdkPackageName() {
+ return mSdkPackageName;
+ }
+
+ /** Getter for {@link #mRequestTime}. */
+ public long getRequestTime() {
+ return mRequestTime;
+ }
+
+ /** Getter for {@link #mIsAdIdPermissionGranted}. */
+ public boolean isAdIdPermissionGranted() {
+ return mIsAdIdPermissionGranted;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WebSourceRegistrationRequestInternal)) return false;
+ WebSourceRegistrationRequestInternal that = (WebSourceRegistrationRequestInternal) o;
+ return Objects.equals(mSourceRegistrationRequest, that.mSourceRegistrationRequest)
+ && Objects.equals(mAppPackageName, that.mAppPackageName)
+ && Objects.equals(mSdkPackageName, that.mSdkPackageName)
+ && mRequestTime == that.mRequestTime
+ && mIsAdIdPermissionGranted == that.mIsAdIdPermissionGranted;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mSourceRegistrationRequest,
+ mAppPackageName,
+ mSdkPackageName,
+ mIsAdIdPermissionGranted);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ mSourceRegistrationRequest.writeToParcel(out, flags);
+ out.writeString(mAppPackageName);
+ out.writeString(mSdkPackageName);
+ out.writeLong(mRequestTime);
+ out.writeBoolean(mIsAdIdPermissionGranted);
+ }
+
+ /** Builder for {@link WebSourceRegistrationRequestInternal}. */
+ public static final class Builder {
+ /** External source registration request from client app SDK. */
+ @NonNull private final WebSourceRegistrationRequest mSourceRegistrationRequest;
+ /** Package name of the app used for the registration. Used to determine the registrant. */
+ @NonNull private final String mAppPackageName;
+ /** Package name of the sdk used for the registration. */
+ @NonNull private final String mSdkPackageName;
+ /** Time the request was created, as millis since boot excluding time in deep sleep. */
+ private final long mRequestTime;
+ /** AD ID Permission Granted. */
+ private boolean mIsAdIdPermissionGranted;
+ /**
+ * Builder constructor for {@link WebSourceRegistrationRequestInternal}.
+ *
+ * @param sourceRegistrationRequest external source registration request
+ * @param appPackageName app package name that is calling PP API
+ * @param sdkPackageName sdk package name that is calling PP API
+ */
+ public Builder(
+ @NonNull WebSourceRegistrationRequest sourceRegistrationRequest,
+ @NonNull String appPackageName,
+ @NonNull String sdkPackageName,
+ long requestTime) {
+ Objects.requireNonNull(sourceRegistrationRequest);
+ Objects.requireNonNull(appPackageName);
+ Objects.requireNonNull(sdkPackageName);
+ mSourceRegistrationRequest = sourceRegistrationRequest;
+ mAppPackageName = appPackageName;
+ mSdkPackageName = sdkPackageName;
+ mRequestTime = requestTime;
+ }
+
+ /** Pre-validates parameters and builds {@link WebSourceRegistrationRequestInternal}. */
+ @NonNull
+ public WebSourceRegistrationRequestInternal build() {
+ return new WebSourceRegistrationRequestInternal(this);
+ }
+
+ /** See {@link WebSourceRegistrationRequestInternal#isAdIdPermissionGranted()}. */
+ public WebSourceRegistrationRequestInternal.Builder setAdIdPermissionGranted(
+ boolean isAdIdPermissionGranted) {
+ mIsAdIdPermissionGranted = isAdIdPermissionGranted;
+ return this;
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/WebTriggerParams.java b/android-34/android/adservices/measurement/WebTriggerParams.java
new file mode 100644
index 0000000..017493b
--- /dev/null
+++ b/android-34/android/adservices/measurement/WebTriggerParams.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/** Class holding trigger registration parameters. */
+public final class WebTriggerParams implements Parcelable {
+ /** Creator for Paracelable (via reflection). */
+ @NonNull
+ public static final Creator<WebTriggerParams> CREATOR =
+ new Creator<WebTriggerParams>() {
+ @Override
+ public WebTriggerParams createFromParcel(Parcel in) {
+ return new WebTriggerParams(in);
+ }
+
+ @Override
+ public WebTriggerParams[] newArray(int size) {
+ return new WebTriggerParams[size];
+ }
+ };
+ /**
+ * URI that the Attribution Reporting API sends a request to in order to obtain trigger
+ * registration parameters.
+ */
+ @NonNull private final Uri mRegistrationUri;
+ /**
+ * Used by the browser to indicate whether the debug key obtained from the registration URI is
+ * allowed to be used.
+ */
+ private final boolean mDebugKeyAllowed;
+
+ private WebTriggerParams(@NonNull Builder builder) {
+ mRegistrationUri = builder.mRegistrationUri;
+ mDebugKeyAllowed = builder.mDebugKeyAllowed;
+ }
+
+ /** Unpack a TriggerRegistration from a Parcel. */
+ private WebTriggerParams(@NonNull Parcel in) {
+ mRegistrationUri = Uri.CREATOR.createFromParcel(in);
+ mDebugKeyAllowed = in.readBoolean();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WebTriggerParams)) return false;
+ WebTriggerParams that = (WebTriggerParams) o;
+ return mDebugKeyAllowed == that.mDebugKeyAllowed
+ && Objects.equals(mRegistrationUri, that.mRegistrationUri);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRegistrationUri, mDebugKeyAllowed);
+ }
+
+ /** Getter for registration Uri. */
+ @NonNull
+ public Uri getRegistrationUri() {
+ return mRegistrationUri;
+ }
+
+ /**
+ * Getter for debug allowed/disallowed flag. Its value as {@code true} means to allow parsing
+ * debug keys from registration responses and their addition in the generated reports.
+ */
+ public boolean isDebugKeyAllowed() {
+ return mDebugKeyAllowed;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ mRegistrationUri.writeToParcel(out, flags);
+ out.writeBoolean(mDebugKeyAllowed);
+ }
+
+ /** A builder for {@link WebTriggerParams}. */
+ public static final class Builder {
+ /**
+ * URI that the Attribution Reporting API sends a request to in order to obtain trigger
+ * registration parameters.
+ */
+ @NonNull private final Uri mRegistrationUri;
+ /**
+ * Used by the browser to indicate whether the debug key obtained from the registration URI
+ * is allowed to be used.
+ */
+ private boolean mDebugKeyAllowed;
+
+ /**
+ * Builder constructor for {@link WebTriggerParams}. {@code mIsDebugKeyAllowed} is assigned
+ * false by default.
+ *
+ * @param registrationUri URI that the Attribution Reporting API sends a request to in order
+ * to obtain trigger registration parameters
+ */
+ public Builder(@NonNull Uri registrationUri) {
+ Objects.requireNonNull(registrationUri);
+ mRegistrationUri = registrationUri;
+ mDebugKeyAllowed = false;
+ }
+
+ /**
+ * Setter for debug allow/disallow flag. Setting it to true will allow parsing debug keys
+ * from registration responses and their addition in the generated reports.
+ *
+ * @param debugKeyAllowed used by the browser to indicate whether the debug key obtained
+ * from the registration URI is allowed to be used
+ * @return builder
+ */
+ @NonNull
+ public Builder setDebugKeyAllowed(boolean debugKeyAllowed) {
+ mDebugKeyAllowed = debugKeyAllowed;
+ return this;
+ }
+
+ /**
+ * Builds immutable {@link WebTriggerParams}.
+ *
+ * @return immutable {@link WebTriggerParams}
+ */
+ @NonNull
+ public WebTriggerParams build() {
+ return new WebTriggerParams(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/WebTriggerRegistrationRequest.java b/android-34/android/adservices/measurement/WebTriggerRegistrationRequest.java
new file mode 100644
index 0000000..fb5cbf0
--- /dev/null
+++ b/android-34/android/adservices/measurement/WebTriggerRegistrationRequest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** Class to hold input to measurement trigger registration calls from web context. */
+public final class WebTriggerRegistrationRequest implements Parcelable {
+ private static final int WEB_TRIGGER_PARAMS_MAX_COUNT = 20;
+
+ /** Creator for Paracelable (via reflection). */
+ @NonNull
+ public static final Parcelable.Creator<WebTriggerRegistrationRequest> CREATOR =
+ new Parcelable.Creator<WebTriggerRegistrationRequest>() {
+ @Override
+ public WebTriggerRegistrationRequest createFromParcel(Parcel in) {
+ return new WebTriggerRegistrationRequest(in);
+ }
+
+ @Override
+ public WebTriggerRegistrationRequest[] newArray(int size) {
+ return new WebTriggerRegistrationRequest[size];
+ }
+ };
+ /** Registration info to fetch sources. */
+ @NonNull private final List<WebTriggerParams> mWebTriggerParams;
+
+ /** Destination {@link Uri}. */
+ @NonNull private final Uri mDestination;
+
+ private WebTriggerRegistrationRequest(@NonNull Builder builder) {
+ mWebTriggerParams = builder.mWebTriggerParams;
+ mDestination = builder.mDestination;
+ }
+
+ private WebTriggerRegistrationRequest(Parcel in) {
+ Objects.requireNonNull(in);
+ ArrayList<WebTriggerParams> webTriggerParams = new ArrayList<>();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ in.readList(webTriggerParams, WebTriggerParams.class.getClassLoader());
+ } else {
+ in.readList(
+ webTriggerParams,
+ WebTriggerParams.class.getClassLoader(),
+ WebTriggerParams.class);
+ }
+ mWebTriggerParams = webTriggerParams;
+ mDestination = Uri.CREATOR.createFromParcel(in);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WebTriggerRegistrationRequest)) return false;
+ WebTriggerRegistrationRequest that = (WebTriggerRegistrationRequest) o;
+ return Objects.equals(mWebTriggerParams, that.mWebTriggerParams)
+ && Objects.equals(mDestination, that.mDestination);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mWebTriggerParams, mDestination);
+ }
+
+ /** Getter for trigger params. */
+ @NonNull
+ public List<WebTriggerParams> getTriggerParams() {
+ return mWebTriggerParams;
+ }
+
+ /** Getter for destination. */
+ @NonNull
+ public Uri getDestination() {
+ return mDestination;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ out.writeList(mWebTriggerParams);
+ mDestination.writeToParcel(out, flags);
+ }
+
+ /** Builder for {@link WebTriggerRegistrationRequest}. */
+ public static final class Builder {
+ /**
+ * Registration info to fetch triggers. Maximum 20 registrations allowed at once, to be in
+ * sync with Chrome platform.
+ */
+ @NonNull private List<WebTriggerParams> mWebTriggerParams;
+ /** Top level origin of publisher app. */
+ @NonNull private final Uri mDestination;
+
+ /**
+ * Builder constructor for {@link WebTriggerRegistrationRequest}.
+ *
+ * @param webTriggerParams contains trigger registration parameters, the list should not be
+ * empty
+ * @param destination trigger destination {@link Uri}
+ */
+ public Builder(@NonNull List<WebTriggerParams> webTriggerParams, @NonNull Uri destination) {
+ Objects.requireNonNull(webTriggerParams);
+ if (webTriggerParams.isEmpty()
+ || webTriggerParams.size() > WEB_TRIGGER_PARAMS_MAX_COUNT) {
+ throw new IllegalArgumentException(
+ "web trigger params size is not within bounds, size: "
+ + webTriggerParams.size());
+ }
+
+ Objects.requireNonNull(destination);
+ if (destination.getScheme() == null) {
+ throw new IllegalArgumentException("Destination origin must have a scheme.");
+ }
+ mWebTriggerParams = webTriggerParams;
+ mDestination = destination;
+
+ }
+
+ /** Pre-validates parameters and builds {@link WebTriggerRegistrationRequest}. */
+ @NonNull
+ public WebTriggerRegistrationRequest build() {
+ return new WebTriggerRegistrationRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/measurement/WebTriggerRegistrationRequestInternal.java b/android-34/android/adservices/measurement/WebTriggerRegistrationRequestInternal.java
new file mode 100644
index 0000000..b92d54c
--- /dev/null
+++ b/android-34/android/adservices/measurement/WebTriggerRegistrationRequestInternal.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.measurement;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Internal trigger registration request object to communicate from {@link MeasurementManager} to
+ * {@link IMeasurementService}.
+ *
+ * @hide
+ */
+public class WebTriggerRegistrationRequestInternal implements Parcelable {
+ /** Creator for Parcelable (via reflection). */
+ @NonNull
+ public static final Creator<WebTriggerRegistrationRequestInternal> CREATOR =
+ new Creator<WebTriggerRegistrationRequestInternal>() {
+ @Override
+ public WebTriggerRegistrationRequestInternal createFromParcel(Parcel in) {
+ return new WebTriggerRegistrationRequestInternal(in);
+ }
+
+ @Override
+ public WebTriggerRegistrationRequestInternal[] newArray(int size) {
+ return new WebTriggerRegistrationRequestInternal[size];
+ }
+ };
+ /** Holds input to measurement trigger registration calls from web context. */
+ @NonNull private final WebTriggerRegistrationRequest mTriggerRegistrationRequest;
+ /** Holds app package info of where the request is coming from. */
+ @NonNull private final String mAppPackageName;
+ /** Holds sdk package info of where the request is coming from. */
+ @NonNull private final String mSdkPackageName;
+ /** AD ID Permission Granted. */
+ private final boolean mIsAdIdPermissionGranted;
+
+ private WebTriggerRegistrationRequestInternal(@NonNull Builder builder) {
+ mTriggerRegistrationRequest = builder.mTriggerRegistrationRequest;
+ mAppPackageName = builder.mAppPackageName;
+ mSdkPackageName = builder.mSdkPackageName;
+ mIsAdIdPermissionGranted = builder.mIsAdIdPermissionGranted;
+ }
+
+ private WebTriggerRegistrationRequestInternal(Parcel in) {
+ Objects.requireNonNull(in);
+ mTriggerRegistrationRequest = WebTriggerRegistrationRequest.CREATOR.createFromParcel(in);
+ mAppPackageName = in.readString();
+ mSdkPackageName = in.readString();
+ mIsAdIdPermissionGranted = in.readBoolean();
+ }
+
+ /** Getter for {@link #mTriggerRegistrationRequest}. */
+ public WebTriggerRegistrationRequest getTriggerRegistrationRequest() {
+ return mTriggerRegistrationRequest;
+ }
+
+ /** Getter for {@link #mAppPackageName}. */
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /** Getter for {@link #mSdkPackageName}. */
+ public String getSdkPackageName() {
+ return mSdkPackageName;
+ }
+
+ /** Getter for {@link #mIsAdIdPermissionGranted}. */
+ public boolean isAdIdPermissionGranted() {
+ return mIsAdIdPermissionGranted;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WebTriggerRegistrationRequestInternal)) return false;
+ WebTriggerRegistrationRequestInternal that = (WebTriggerRegistrationRequestInternal) o;
+ return Objects.equals(mTriggerRegistrationRequest, that.mTriggerRegistrationRequest)
+ && Objects.equals(mAppPackageName, that.mAppPackageName)
+ && Objects.equals(mSdkPackageName, that.mSdkPackageName)
+ && Objects.equals(mIsAdIdPermissionGranted, that.mIsAdIdPermissionGranted);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mTriggerRegistrationRequest,
+ mAppPackageName,
+ mSdkPackageName,
+ mIsAdIdPermissionGranted);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ Objects.requireNonNull(out);
+ mTriggerRegistrationRequest.writeToParcel(out, flags);
+ out.writeString(mAppPackageName);
+ out.writeString(mSdkPackageName);
+ out.writeBoolean(mIsAdIdPermissionGranted);
+ }
+
+ /** Builder for {@link WebTriggerRegistrationRequestInternal}. */
+ public static final class Builder {
+ /** External trigger registration request from client app SDK. */
+ @NonNull private final WebTriggerRegistrationRequest mTriggerRegistrationRequest;
+ /** Package name of the app used for the registration. Used to determine the registrant. */
+ @NonNull private final String mAppPackageName;
+ /** Package name of the sdk used for the registration. */
+ @NonNull private final String mSdkPackageName;
+ /** AD ID Permission Granted. */
+ private boolean mIsAdIdPermissionGranted;
+
+ /**
+ * Builder constructor for {@link WebTriggerRegistrationRequestInternal}.
+ *
+ * @param triggerRegistrationRequest external trigger registration request
+ * @param appPackageName app package name that is calling PP API
+ * @param sdkPackageName sdk package name that is calling PP API
+ */
+ public Builder(
+ @NonNull WebTriggerRegistrationRequest triggerRegistrationRequest,
+ @NonNull String appPackageName,
+ @NonNull String sdkPackageName) {
+ Objects.requireNonNull(triggerRegistrationRequest);
+ Objects.requireNonNull(appPackageName);
+ Objects.requireNonNull(sdkPackageName);
+ mTriggerRegistrationRequest = triggerRegistrationRequest;
+ mAppPackageName = appPackageName;
+ mSdkPackageName = sdkPackageName;
+ }
+
+ /** Pre-validates parameters and builds {@link WebTriggerRegistrationRequestInternal}. */
+ @NonNull
+ public WebTriggerRegistrationRequestInternal build() {
+ return new WebTriggerRegistrationRequestInternal(this);
+ }
+
+ /** See {@link WebTriggerRegistrationRequestInternal#isAdIdPermissionGranted()}. */
+ public WebTriggerRegistrationRequestInternal.Builder setAdIdPermissionGranted(
+ boolean isAdIdPermissionGranted) {
+ mIsAdIdPermissionGranted = isAdIdPermissionGranted;
+ return this;
+ }
+ }
+}
diff --git a/android-34/android/adservices/topics/GetTopicsParam.java b/android-34/android/adservices/topics/GetTopicsParam.java
new file mode 100644
index 0000000..ce80436
--- /dev/null
+++ b/android-34/android/adservices/topics/GetTopicsParam.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.topics;
+
+import static android.adservices.topics.TopicsManager.EMPTY_SDK;
+import static android.adservices.topics.TopicsManager.RECORD_OBSERVATION_DEFAULT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represent input params to the getTopics API.
+ *
+ * @hide
+ */
+public final class GetTopicsParam implements Parcelable {
+ private final String mSdkName;
+ private final String mSdkPackageName;
+ private final String mAppPackageName;
+ private final boolean mRecordObservation;
+
+ private GetTopicsParam(
+ @NonNull String sdkName,
+ @Nullable String sdkPackageName,
+ @NonNull String appPackageName,
+ boolean recordObservation) {
+ mSdkName = sdkName;
+ mSdkPackageName = sdkPackageName;
+ mAppPackageName = appPackageName;
+ mRecordObservation = recordObservation;
+ }
+
+ private GetTopicsParam(@NonNull Parcel in) {
+ mSdkName = in.readString();
+ mSdkPackageName = in.readString();
+ mAppPackageName = in.readString();
+ mRecordObservation = in.readBoolean();
+ }
+
+ public static final @NonNull Creator<GetTopicsParam> CREATOR =
+ new Parcelable.Creator<GetTopicsParam>() {
+ @Override
+ public GetTopicsParam createFromParcel(Parcel in) {
+ return new GetTopicsParam(in);
+ }
+
+ @Override
+ public GetTopicsParam[] newArray(int size) {
+ return new GetTopicsParam[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeString(mSdkName);
+ out.writeString(mSdkPackageName);
+ out.writeString(mAppPackageName);
+ out.writeBoolean(mRecordObservation);
+ }
+
+ /** Get the Sdk Name. This is the name in the <sdk-library> tag of the Manifest. */
+ @NonNull
+ public String getSdkName() {
+ return mSdkName;
+ }
+
+ /** Get the Sdk Package Name. This is the package name in the Manifest. */
+ @NonNull
+ public String getSdkPackageName() {
+ return mSdkPackageName;
+ }
+
+ /** Get the App PackageName. */
+ @NonNull
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /** Get the Record Observation. */
+ public boolean shouldRecordObservation() {
+ return mRecordObservation;
+ }
+
+ /** Builder for {@link GetTopicsParam} objects. */
+ public static final class Builder {
+ private String mSdkName;
+ private String mSdkPackageName;
+ private String mAppPackageName;
+ private boolean mRecordObservation = RECORD_OBSERVATION_DEFAULT;
+
+ public Builder() {}
+
+ /**
+ * Set the Sdk Name. When the app calls the Topics API directly without using a SDK, don't
+ * set this field.
+ */
+ public @NonNull Builder setSdkName(@NonNull String sdkName) {
+ mSdkName = sdkName;
+ return this;
+ }
+
+ /**
+ * Set the Sdk Package Name. When the app calls the Topics API directly without using an
+ * SDK, don't set this field.
+ */
+ public @NonNull Builder setSdkPackageName(@NonNull String sdkPackageName) {
+ mSdkPackageName = sdkPackageName;
+ return this;
+ }
+
+ /** Set the App PackageName. */
+ public @NonNull Builder setAppPackageName(@NonNull String appPackageName) {
+ mAppPackageName = appPackageName;
+ return this;
+ }
+
+ /**
+ * Set the Record Observation. Whether to record that the caller has observed the topics of
+ * the host app or not. This will be used to determine if the caller can receive the topic
+ * in the next epoch.
+ */
+ public @NonNull Builder setShouldRecordObservation(boolean recordObservation) {
+ mRecordObservation = recordObservation;
+ return this;
+ }
+
+ /** Builds a {@link GetTopicsParam} instance. */
+ public @NonNull GetTopicsParam build() {
+ if (mSdkName == null) {
+ // When Sdk name is not set, we assume the App calls the Topics API directly.
+ // We set the Sdk name to empty to mark this.
+ mSdkName = EMPTY_SDK;
+ }
+
+ if (mSdkPackageName == null) {
+ // When Sdk package name is not set, we assume the App calls the Topics API
+ // directly.
+ // We set the Sdk package name to empty to mark this.
+ mSdkPackageName = EMPTY_SDK;
+ }
+
+ if (mAppPackageName == null || mAppPackageName.isEmpty()) {
+ throw new IllegalArgumentException("App PackageName must not be empty or null");
+ }
+
+ return new GetTopicsParam(
+ mSdkName, mSdkPackageName, mAppPackageName, mRecordObservation);
+ }
+ }
+}
diff --git a/android-34/android/adservices/topics/GetTopicsRequest.java b/android-34/android/adservices/topics/GetTopicsRequest.java
new file mode 100644
index 0000000..cc8e51b
--- /dev/null
+++ b/android-34/android/adservices/topics/GetTopicsRequest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.topics;
+
+import static android.adservices.topics.TopicsManager.EMPTY_SDK;
+import static android.adservices.topics.TopicsManager.RECORD_OBSERVATION_DEFAULT;
+
+import android.annotation.NonNull;
+
+/** Get Topics Request. */
+public final class GetTopicsRequest {
+
+ /** Name of Ads SDK that is involved in this request. */
+ private final String mAdsSdkName;
+
+ /** Whether to record that the caller has observed the topics of the host app or not. */
+ private final boolean mRecordObservation;
+
+ private GetTopicsRequest(@NonNull Builder builder) {
+ mAdsSdkName = builder.mAdsSdkName;
+ mRecordObservation = builder.mRecordObservation;
+ }
+
+ /** Get the Sdk Name. */
+ @NonNull
+ public String getAdsSdkName() {
+ return mAdsSdkName;
+ }
+
+ /** Get Record Observation. */
+ public boolean shouldRecordObservation() {
+ return mRecordObservation;
+ }
+
+ /** Builder for {@link GetTopicsRequest} objects. */
+ public static final class Builder {
+ private String mAdsSdkName = EMPTY_SDK;
+ private boolean mRecordObservation = RECORD_OBSERVATION_DEFAULT;
+
+ /** Creates a {@link Builder} for {@link GetTopicsRequest} objects. */
+ public Builder() {}
+
+ /**
+ * Set Ads Sdk Name.
+ *
+ * <p>This must be called by SDKs running outside of the Sandbox. Other clients must not
+ * call it.
+ *
+ * @param adsSdkName the Ads Sdk Name.
+ */
+ @NonNull
+ public Builder setAdsSdkName(@NonNull String adsSdkName) {
+ // This is the case the SDK calling from outside of the Sandbox.
+ // Check if the caller set the adsSdkName
+ if (adsSdkName == null) {
+ throw new IllegalArgumentException(
+ "When calling Topics API outside of the Sandbox, caller should set Ads Sdk"
+ + " Name");
+ }
+
+ mAdsSdkName = adsSdkName;
+ return this;
+ }
+
+ /**
+ * Set the Record Observation.
+ *
+ * @param recordObservation whether to record that the caller has observed the topics of the
+ * host app or not. This will be used to determine if the caller can receive the topic
+ * in the next epoch.
+ */
+ @NonNull
+ public Builder setShouldRecordObservation(boolean recordObservation) {
+ mRecordObservation = recordObservation;
+ return this;
+ }
+
+ /** Builds a {@link GetTopicsRequest} instance. */
+ @NonNull
+ public GetTopicsRequest build() {
+ return new GetTopicsRequest(this);
+ }
+ }
+}
diff --git a/android-34/android/adservices/topics/GetTopicsResponse.java b/android-34/android/adservices/topics/GetTopicsResponse.java
new file mode 100644
index 0000000..d683915
--- /dev/null
+++ b/android-34/android/adservices/topics/GetTopicsResponse.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.topics;
+import android.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** Represent the result from the getTopics API. */
+public final class GetTopicsResponse {
+ /** List of Topic objects returned by getTopics API. */
+ private final List<Topic> mTopics;
+
+ private GetTopicsResponse(@NonNull List<Topic> topics) {
+ mTopics = topics;
+ }
+
+ /** Returns a {@link List} of {@link Topic} objects returned by getTopics API. */
+ @NonNull
+ public List<Topic> getTopics() {
+ return mTopics;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof GetTopicsResponse)) {
+ return false;
+ }
+ GetTopicsResponse that = (GetTopicsResponse) o;
+ return mTopics.equals(that.mTopics);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTopics);
+ }
+
+ /**
+ * Builder for {@link GetTopicsResponse} objects. This class should be used in test
+ * implementation as expected response from Topics API
+ */
+ public static final class Builder {
+ private List<Topic> mTopics = new ArrayList<>();
+
+ /**
+ * Creates a {@link Builder} for {@link GetTopicsResponse} objects.
+ *
+ * @param topics The list of the returned Topics.
+ */
+ public Builder(@NonNull List<Topic> topics) {
+ mTopics = Objects.requireNonNull(topics);
+ }
+
+ /**
+ * Builds a {@link GetTopicsResponse} instance.
+ *
+ * <p>throws IllegalArgumentException if any of the params are null or there is any mismatch
+ * in the size of ModelVersions and TaxonomyVersions.
+ */
+ public @NonNull GetTopicsResponse build() {
+ if (mTopics == null) {
+ throw new IllegalArgumentException("Topics is null");
+ }
+ return new GetTopicsResponse(mTopics);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android-34/android/adservices/topics/GetTopicsResult.java b/android-34/android/adservices/topics/GetTopicsResult.java
new file mode 100644
index 0000000..96e620b
--- /dev/null
+++ b/android-34/android/adservices/topics/GetTopicsResult.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.adservices.topics;
+
+import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
+
+import android.adservices.common.AdServicesResponse;
+import android.adservices.common.AdServicesStatusUtils;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represent the result from the getTopics API.
+ *
+ * @hide
+ */
+public final class GetTopicsResult extends AdServicesResponse {
+ private final List<Long> mTaxonomyVersions;
+ private final List<Long> mModelVersions;
+ private final List<Integer> mTopics;
+
+ private GetTopicsResult(
+ @AdServicesStatusUtils.StatusCode int resultCode,
+ @Nullable String errorMessage,
+ @NonNull List<Long> taxonomyVersions,
+ @NonNull List<Long> modelVersions,
+ @NonNull List<Integer> topics) {
+ super(resultCode, errorMessage);
+ mTaxonomyVersions = taxonomyVersions;
+ mModelVersions = modelVersions;
+ mTopics = topics;
+ }
+
+ private GetTopicsResult(@NonNull Parcel in) {
+ super(in.readInt(), in.readString());
+
+ mTaxonomyVersions = Collections.unmodifiableList(readLongList(in));
+ mModelVersions = Collections.unmodifiableList(readLongList(in));
+ mTopics = Collections.unmodifiableList(readIntegerList(in));
+ }
+
+ public static final @NonNull Creator<GetTopicsResult> CREATOR =
+ new Parcelable.Creator<GetTopicsResult>() {
+ @Override
+ public GetTopicsResult createFromParcel(Parcel in) {
+ return new GetTopicsResult(in);
+ }
+
+ @Override
+ public GetTopicsResult[] newArray(int size) {
+ return new GetTopicsResult[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mStatusCode);
+ out.writeString(mErrorMessage);
+ writeLongList(out, mTaxonomyVersions);
+ writeLongList(out, mModelVersions);
+ writeIntegerList(out, mTopics);
+ }
+
+ /**
+ * Returns {@code true} if {@link #getResultCode} equals {@link
+ * AdServicesStatusUtils#STATUS_SUCCESS}.
+ */
+ public boolean isSuccess() {
+ return getResultCode() == STATUS_SUCCESS;
+ }
+
+ /** Returns one of the {@code RESULT} constants defined in {@link GetTopicsResult}. */
+ public @AdServicesStatusUtils.StatusCode int getResultCode() {
+ return mStatusCode;
+ }
+
+ /**
+ * Returns the error message associated with this result.
+ *
+ * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. The error
+ * message may be {@code null} even if {@link #isSuccess} is {@code false}.
+ */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /** Get the Taxonomy Versions. */
+ public List<Long> getTaxonomyVersions() {
+ return mTaxonomyVersions;
+ }
+
+ /** Get the Model Versions. */
+ public List<Long> getModelVersions() {
+ return mModelVersions;
+ }
+
+ @NonNull
+ public List<Integer> getTopics() {
+ return mTopics;
+ }
+
+ @Override
+ public String toString() {
+ return "GetTopicsResult{"
+ + "mResultCode="
+ + mStatusCode
+ + ", mErrorMessage='"
+ + mErrorMessage
+ + '\''
+ + ", mTaxonomyVersions="
+ + mTaxonomyVersions
+ + ", mModelVersions="
+ + mModelVersions
+ + ", mTopics="
+ + mTopics
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof GetTopicsResult)) {
+ return false;
+ }
+
+ GetTopicsResult that = (GetTopicsResult) o;
+
+ return mStatusCode == that.mStatusCode
+ && Objects.equals(mErrorMessage, that.mErrorMessage)
+ && mTaxonomyVersions.equals(that.mTaxonomyVersions)
+ && mModelVersions.equals(that.mModelVersions)
+ && mTopics.equals(that.mTopics);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mStatusCode, mErrorMessage, mTaxonomyVersions, mModelVersions, mTopics);
+ }
+
+ // Read the list of long from parcel.
+ private static List<Long> readLongList(@NonNull Parcel in) {
+ List<Long> list = new ArrayList<>();
+
+ int toReadCount = in.readInt();
+ // Negative toReadCount is handled implicitly
+ for (int i = 0; i < toReadCount; i++) {
+ list.add(in.readLong());
+ }
+
+ return list;
+ }
+
+ // Read the list of integer from parcel.
+ private static List<Integer> readIntegerList(@NonNull Parcel in) {
+ List<Integer> list = new ArrayList<>();
+
+ int toReadCount = in.readInt();
+ // Negative toReadCount is handled implicitly
+ for (int i = 0; i < toReadCount; i++) {
+ list.add(in.readInt());
+ }
+
+ return list;
+ }
+
+ // Write a List of Long to parcel.
+ private static void writeLongList(@NonNull Parcel out, @Nullable List<Long> val) {
+ if (val == null) {
+ out.writeInt(-1);
+ return;
+ }
+ out.writeInt(val.size());
+ for (Long l : val) {
+ out.writeLong(l);
+ }
+ }
+
+ // Write a List of Integer to parcel.
+ private static void writeIntegerList(@NonNull Parcel out, @Nullable List<Integer> val) {
+ if (val == null) {
+ out.writeInt(-1);
+ return;
+ }
+ out.writeInt(val.size());
+ for (Integer integer : val) {
+ out.writeInt(integer);
+ }
+ }
+
+ /**
+ * Builder for {@link GetTopicsResult} objects.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ private @AdServicesStatusUtils.StatusCode int mResultCode;
+ @Nullable private String mErrorMessage;
+ private List<Long> mTaxonomyVersions = new ArrayList<>();
+ private List<Long> mModelVersions = new ArrayList<>();
+ private List<Integer> mTopics = new ArrayList<>();
+
+ public Builder() {}
+
+ /** Set the Result Code. */
+ public @NonNull Builder setResultCode(@AdServicesStatusUtils.StatusCode int resultCode) {
+ mResultCode = resultCode;
+ return this;
+ }
+
+ /** Set the Error Message. */
+ public @NonNull Builder setErrorMessage(@Nullable String errorMessage) {
+ mErrorMessage = errorMessage;
+ return this;
+ }
+
+ /** Set the Taxonomy Version. */
+ public @NonNull Builder setTaxonomyVersions(@NonNull List<Long> taxonomyVersions) {
+ mTaxonomyVersions = taxonomyVersions;
+ return this;
+ }
+
+ /** Set the Model Version. */
+ public @NonNull Builder setModelVersions(@NonNull List<Long> modelVersions) {
+ mModelVersions = modelVersions;
+ return this;
+ }
+
+ /** Set the list of the returned Topics */
+ public @NonNull Builder setTopics(@NonNull List<Integer> topics) {
+ mTopics = topics;
+ return this;
+ }
+
+ /**
+ * Builds a {@link GetTopicsResult} instance.
+ *
+ * <p>throws IllegalArgumentException if any of the params are null or there is any mismatch
+ * in the size of ModelVersions and TaxonomyVersions.
+ */
+ public @NonNull GetTopicsResult build() {
+ if (mTopics == null || mTaxonomyVersions == null || mModelVersions == null) {
+ throw new IllegalArgumentException(
+ "Topics or TaxonomyVersion or ModelVersion is null");
+ }
+
+ if (mTopics.size() != mTaxonomyVersions.size()
+ || mTopics.size() != mModelVersions.size()) {
+ throw new IllegalArgumentException("Size mismatch in Topics");
+ }
+
+ return new GetTopicsResult(
+ mResultCode, mErrorMessage, mTaxonomyVersions, mModelVersions, mTopics);
+ }
+ }
+}
diff --git a/android-34/android/adservices/topics/Topic.java b/android-34/android/adservices/topics/Topic.java
new file mode 100644
index 0000000..593762a
--- /dev/null
+++ b/android-34/android/adservices/topics/Topic.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.topics;
+
+import java.util.Objects;
+
+/** Represent the topic result from the getTopics API. */
+public final class Topic {
+ private final long mTaxonomyVersion;
+ private final long mModelVersion;
+ private final int mTopicId;
+
+ /**
+ * Creates an object which represents the result from the getTopics API.
+ *
+ * @param mTaxonomyVersion a long representing the version of the taxonomy.
+ * @param mModelVersion a long representing the version of the model.
+ * @param mTopicId an integer representing the unique id of a topic.
+ */
+ public Topic(long mTaxonomyVersion, long mModelVersion, int mTopicId) {
+ this.mTaxonomyVersion = mTaxonomyVersion;
+ this.mModelVersion = mModelVersion;
+ this.mTopicId = mTopicId;
+ }
+
+ /** Get the ModelVersion. */
+ public long getModelVersion() {
+ return mModelVersion;
+ }
+
+ /** Get the TaxonomyVersion. */
+ public long getTaxonomyVersion() {
+ return mTaxonomyVersion;
+ }
+
+ /** Get the Topic ID. */
+ public int getTopicId() {
+ return mTopicId;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) return true;
+ if (!(object instanceof Topic)) return false;
+ Topic topic = (Topic) object;
+ return getTaxonomyVersion() == topic.getTaxonomyVersion()
+ && getModelVersion() == topic.getModelVersion()
+ && getTopicId() == topic.getTopicId();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getTaxonomyVersion(), getModelVersion(), getTopicId());
+ }
+
+ @Override
+ public java.lang.String toString() {
+ return "Topic{"
+ + "mTaxonomyVersion="
+ + mTaxonomyVersion
+ + ", mModelVersion="
+ + mModelVersion
+ + ", mTopicCode="
+ + mTopicId
+ + '}';
+ }
+}
diff --git a/android-34/android/adservices/topics/TopicsManager.java b/android-34/android/adservices/topics/TopicsManager.java
new file mode 100644
index 0000000..bf7ea22
--- /dev/null
+++ b/android-34/android/adservices/topics/TopicsManager.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.adservices.topics;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS;
+import static android.adservices.common.AdServicesStatusUtils.ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE;
+
+import android.adservices.common.AdServicesStatusUtils;
+import android.adservices.common.CallerMetadata;
+import android.adservices.common.SandboxedSdkContextUtils;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.TestApi;
+import android.app.sdksandbox.SandboxedSdkContext;
+import android.content.Context;
+import android.os.Build;
+import android.os.LimitExceededException;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.LoggerFactory;
+import com.android.adservices.ServiceBinder;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * TopicsManager provides APIs for App and Ad-Sdks to get the user interest topics in a privacy
+ * preserving way.
+ *
+ * <p>The instance of the {@link TopicsManager} can be obtained using {@link
+ * Context#getSystemService} and {@link TopicsManager} class.
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public final class TopicsManager {
+ private static final LoggerFactory.Logger sLogger = LoggerFactory.getTopicsLogger();
+ /**
+ * Constant that represents the service name for {@link TopicsManager} to be used in {@link
+ * android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers}
+ *
+ * @hide
+ */
+ public static final String TOPICS_SERVICE = "topics_service";
+
+ // When an app calls the Topics API directly, it sets the SDK name to empty string.
+ static final String EMPTY_SDK = "";
+
+ // Default value is true to record SDK's Observation when it calls Topics API.
+ static final boolean RECORD_OBSERVATION_DEFAULT = true;
+
+ private Context mContext;
+ private ServiceBinder<ITopicsService> mServiceBinder;
+
+ /**
+ * Factory method for creating an instance of TopicsManager.
+ *
+ * @param context The {@link Context} to use
+ * @return A {@link TopicsManager} instance
+ */
+ @NonNull
+ public static TopicsManager get(@NonNull Context context) {
+ // TODO(b/269798827): Enable for R.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ throw new IllegalStateException(ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE);
+ }
+ // On TM+, context.getSystemService() does more than just call constructor.
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ ? context.getSystemService(TopicsManager.class)
+ : new TopicsManager(context);
+ }
+
+ /**
+ * Create TopicsManager
+ *
+ * @hide
+ */
+ public TopicsManager(Context context) {
+ // TODO(b/269798827): Enable for R.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ throw new IllegalStateException(ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE);
+ }
+ // In case the TopicsManager is initiated from inside a sdk_sandbox process the fields
+ // will be immediately rewritten by the initialize method below.
+ initialize(context);
+ }
+
+ /**
+ * Initializes {@link TopicsManager} with the given {@code context}.
+ *
+ * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context.
+ * For more information check the javadoc on the {@link
+ * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}.
+ *
+ * @hide
+ * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry
+ */
+ public TopicsManager initialize(Context context) {
+ mContext = context;
+ mServiceBinder =
+ ServiceBinder.getServiceBinder(
+ context,
+ AdServicesCommon.ACTION_TOPICS_SERVICE,
+ ITopicsService.Stub::asInterface);
+ return this;
+ }
+
+ @NonNull
+ private ITopicsService getService() {
+ ITopicsService service = mServiceBinder.getService();
+ if (service == null) {
+ throw new IllegalStateException(ILLEGAL_STATE_EXCEPTION_ERROR_MESSAGE);
+ }
+ return service;
+ }
+
+ /**
+ * Return the topics.
+ *
+ * @param getTopicsRequest The request for obtaining Topics.
+ * @param executor The executor to run callback.
+ * @param callback The callback that's called after topics are available or an error occurs.
+ * @throws SecurityException if caller is not authorized to call this API.
+ * @throws IllegalStateException if this API is not available.
+ * @throws LimitExceededException if rate limit was reached.
+ */
+ @NonNull
+ @RequiresPermission(ACCESS_ADSERVICES_TOPICS)
+ public void getTopics(
+ @NonNull GetTopicsRequest getTopicsRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<GetTopicsResponse, Exception> callback) {
+ Objects.requireNonNull(getTopicsRequest);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ CallerMetadata callerMetadata =
+ new CallerMetadata.Builder()
+ .setBinderElapsedTimestamp(SystemClock.elapsedRealtime())
+ .build();
+ final ITopicsService service = getService();
+ String sdkName = getTopicsRequest.getAdsSdkName();
+ String appPackageName = "";
+ String sdkPackageName = "";
+ // First check if context is SandboxedSdkContext or not
+ SandboxedSdkContext sandboxedSdkContext =
+ SandboxedSdkContextUtils.getAsSandboxedSdkContext(mContext);
+ if (sandboxedSdkContext != null) {
+ // This is the case with the Sandbox.
+ sdkPackageName = sandboxedSdkContext.getSdkPackageName();
+ appPackageName = sandboxedSdkContext.getClientPackageName();
+
+ if (!TextUtils.isEmpty(sdkName)) {
+ throw new IllegalArgumentException(
+ "When calling Topics API from Sandbox, caller should not set Ads Sdk Name");
+ }
+
+ String sdkNameFromSandboxedContext = sandboxedSdkContext.getSdkName();
+ if (null == sdkNameFromSandboxedContext || sdkNameFromSandboxedContext.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Sdk Name From SandboxedSdkContext should not be null or empty");
+ }
+
+ sdkName = sdkNameFromSandboxedContext;
+ } else {
+ // This is the case without the Sandbox.
+ if (null == sdkName) {
+ // When adsSdkName is not set, we assume the App calls the Topics API directly.
+ // We set the adsSdkName to empty to mark this.
+ sdkName = EMPTY_SDK;
+ }
+ appPackageName = mContext.getPackageName();
+ }
+ try {
+ service.getTopics(
+ new GetTopicsParam.Builder()
+ .setAppPackageName(appPackageName)
+ .setSdkName(sdkName)
+ .setSdkPackageName(sdkPackageName)
+ .setShouldRecordObservation(getTopicsRequest.shouldRecordObservation())
+ .build(),
+ callerMetadata,
+ new IGetTopicsCallback.Stub() {
+ @Override
+ public void onResult(GetTopicsResult resultParcel) {
+ executor.execute(
+ () -> {
+ if (resultParcel.isSuccess()) {
+ callback.onResult(
+ new GetTopicsResponse.Builder(
+ getTopicList(resultParcel))
+ .build());
+ } else {
+ // TODO: Errors should be returned in onFailure method.
+ callback.onError(
+ AdServicesStatusUtils.asException(
+ resultParcel));
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(int resultCode) {
+ executor.execute(
+ () ->
+ callback.onError(
+ AdServicesStatusUtils.asException(resultCode)));
+ }
+ });
+ } catch (RemoteException e) {
+ sLogger.e(e, "RemoteException");
+ callback.onError(e);
+ }
+ }
+
+ private List<Topic> getTopicList(GetTopicsResult resultParcel) {
+ List<Long> taxonomyVersionsList = resultParcel.getTaxonomyVersions();
+ List<Long> modelVersionsList = resultParcel.getModelVersions();
+ List<Integer> topicsCodeList = resultParcel.getTopics();
+ List<Topic> topicList = new ArrayList<>();
+ int size = taxonomyVersionsList.size();
+ for (int i = 0; i < size; i++) {
+ Topic topic =
+ new Topic(
+ taxonomyVersionsList.get(i),
+ modelVersionsList.get(i),
+ topicsCodeList.get(i));
+ topicList.add(topic);
+ }
+
+ return topicList;
+ }
+
+ /**
+ * If the service is in an APK (as opposed to the system service), unbind it from the service to
+ * allow the APK process to die.
+ *
+ * @hide Not sure if we'll need this functionality in the final API. For now, we need it for
+ * performance testing to simulate "cold-start" situations.
+ */
+ // TODO: change to @VisibleForTesting
+ @TestApi
+ public void unbindFromService() {
+ mServiceBinder.unbindFromService();
+ }
+}
diff --git a/android-34/android/apex/ApexInfo.java b/android-34/android/apex/ApexInfo.java
new file mode 100644
index 0000000..8450c52
--- /dev/null
+++ b/android-34/android/apex/ApexInfo.java
@@ -0,0 +1,92 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package android.apex;
+public class ApexInfo implements android.os.Parcelable
+{
+ public java.lang.String moduleName;
+ public java.lang.String modulePath;
+ public java.lang.String preinstalledModulePath;
+ public long versionCode = 0L;
+ public java.lang.String versionName;
+ public boolean isFactory = false;
+ public boolean isActive = false;
+ // Populated only for getStagedApex() API
+ public boolean hasClassPathJars = false;
+ // Will be set to true if during this boot a different APEX package of the APEX was
+ // activated, than in the previous boot.
+ // This can happen in the following situations:
+ // 1. It was part of the staged session that was applied during this boot.
+ // 2. A compressed system APEX was decompressed during this boot.
+ // 3. apexd failed to activate an APEX on /data/apex/active (that was successfully
+ // activated during last boot) and needed to fallback to pre-installed counterpart.
+ // Note: this field can only be set to true during boot, after boot is completed
+ // (sys.boot_completed = 1) value of this field will always be false.
+ public boolean activeApexChanged = false;
+ public static final android.os.Parcelable.Creator<ApexInfo> CREATOR = new android.os.Parcelable.Creator<ApexInfo>() {
+ @Override
+ public ApexInfo createFromParcel(android.os.Parcel _aidl_source) {
+ ApexInfo _aidl_out = new ApexInfo();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public ApexInfo[] newArray(int _aidl_size) {
+ return new ApexInfo[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(android.os.Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeString(moduleName);
+ _aidl_parcel.writeString(modulePath);
+ _aidl_parcel.writeString(preinstalledModulePath);
+ _aidl_parcel.writeLong(versionCode);
+ _aidl_parcel.writeString(versionName);
+ _aidl_parcel.writeInt(((isFactory)?(1):(0)));
+ _aidl_parcel.writeInt(((isActive)?(1):(0)));
+ _aidl_parcel.writeInt(((hasClassPathJars)?(1):(0)));
+ _aidl_parcel.writeInt(((activeApexChanged)?(1):(0)));
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ public final void readFromParcel(android.os.Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ moduleName = _aidl_parcel.readString();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ modulePath = _aidl_parcel.readString();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ preinstalledModulePath = _aidl_parcel.readString();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ versionCode = _aidl_parcel.readLong();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ versionName = _aidl_parcel.readString();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isFactory = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isActive = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ hasClassPathJars = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ activeApexChanged = (0!=_aidl_parcel.readInt());
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ return _mask;
+ }
+}
diff --git a/android-34/android/apex/ApexInfoList.java b/android-34/android/apex/ApexInfoList.java
new file mode 100644
index 0000000..6111255
--- /dev/null
+++ b/android-34/android/apex/ApexInfoList.java
@@ -0,0 +1,65 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package android.apex;
+public class ApexInfoList implements android.os.Parcelable
+{
+ public android.apex.ApexInfo[] apexInfos;
+ public static final android.os.Parcelable.Creator<ApexInfoList> CREATOR = new android.os.Parcelable.Creator<ApexInfoList>() {
+ @Override
+ public ApexInfoList createFromParcel(android.os.Parcel _aidl_source) {
+ ApexInfoList _aidl_out = new ApexInfoList();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public ApexInfoList[] newArray(int _aidl_size) {
+ return new ApexInfoList[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(android.os.Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeTypedArray(apexInfos, _aidl_flag);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ public final void readFromParcel(android.os.Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ apexInfos = _aidl_parcel.createTypedArray(android.apex.ApexInfo.CREATOR);
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ _mask |= describeContents(apexInfos);
+ return _mask;
+ }
+ private int describeContents(Object _v) {
+ if (_v == null) return 0;
+ if (_v instanceof Object[]) {
+ int _mask = 0;
+ for (Object o : (Object[]) _v) {
+ _mask |= describeContents(o);
+ }
+ return _mask;
+ }
+ if (_v instanceof android.os.Parcelable) {
+ return ((android.os.Parcelable) _v).describeContents();
+ }
+ return 0;
+ }
+}
diff --git a/android-34/android/apex/ApexSessionInfo.java b/android-34/android/apex/ApexSessionInfo.java
new file mode 100644
index 0000000..e2fb169
--- /dev/null
+++ b/android-34/android/apex/ApexSessionInfo.java
@@ -0,0 +1,95 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package android.apex;
+public class ApexSessionInfo implements android.os.Parcelable
+{
+ public int sessionId = 0;
+ // Maps to apex::proto::SessionState::State enum.
+ public boolean isUnknown = false;
+ public boolean isVerified = false;
+ public boolean isStaged = false;
+ public boolean isActivated = false;
+ public boolean isRevertInProgress = false;
+ public boolean isActivationFailed = false;
+ public boolean isSuccess = false;
+ public boolean isReverted = false;
+ public boolean isRevertFailed = false;
+ public java.lang.String crashingNativeProcess;
+ public java.lang.String errorMessage;
+ public static final android.os.Parcelable.Creator<ApexSessionInfo> CREATOR = new android.os.Parcelable.Creator<ApexSessionInfo>() {
+ @Override
+ public ApexSessionInfo createFromParcel(android.os.Parcel _aidl_source) {
+ ApexSessionInfo _aidl_out = new ApexSessionInfo();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public ApexSessionInfo[] newArray(int _aidl_size) {
+ return new ApexSessionInfo[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(android.os.Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeInt(sessionId);
+ _aidl_parcel.writeInt(((isUnknown)?(1):(0)));
+ _aidl_parcel.writeInt(((isVerified)?(1):(0)));
+ _aidl_parcel.writeInt(((isStaged)?(1):(0)));
+ _aidl_parcel.writeInt(((isActivated)?(1):(0)));
+ _aidl_parcel.writeInt(((isRevertInProgress)?(1):(0)));
+ _aidl_parcel.writeInt(((isActivationFailed)?(1):(0)));
+ _aidl_parcel.writeInt(((isSuccess)?(1):(0)));
+ _aidl_parcel.writeInt(((isReverted)?(1):(0)));
+ _aidl_parcel.writeInt(((isRevertFailed)?(1):(0)));
+ _aidl_parcel.writeString(crashingNativeProcess);
+ _aidl_parcel.writeString(errorMessage);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ public final void readFromParcel(android.os.Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ sessionId = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isUnknown = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isVerified = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isStaged = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isActivated = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isRevertInProgress = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isActivationFailed = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isSuccess = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isReverted = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isRevertFailed = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ crashingNativeProcess = _aidl_parcel.readString();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ errorMessage = _aidl_parcel.readString();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ return _mask;
+ }
+}
diff --git a/android-34/android/apex/ApexSessionParams.java b/android-34/android/apex/ApexSessionParams.java
new file mode 100644
index 0000000..12b9e8e
--- /dev/null
+++ b/android-34/android/apex/ApexSessionParams.java
@@ -0,0 +1,66 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package android.apex;
+public class ApexSessionParams implements android.os.Parcelable
+{
+ public int sessionId = 0;
+ public int[] childSessionIds = {};
+ public boolean hasRollbackEnabled = false;
+ public boolean isRollback = false;
+ public int rollbackId = 0;
+ public static final android.os.Parcelable.Creator<ApexSessionParams> CREATOR = new android.os.Parcelable.Creator<ApexSessionParams>() {
+ @Override
+ public ApexSessionParams createFromParcel(android.os.Parcel _aidl_source) {
+ ApexSessionParams _aidl_out = new ApexSessionParams();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public ApexSessionParams[] newArray(int _aidl_size) {
+ return new ApexSessionParams[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(android.os.Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeInt(sessionId);
+ _aidl_parcel.writeIntArray(childSessionIds);
+ _aidl_parcel.writeInt(((hasRollbackEnabled)?(1):(0)));
+ _aidl_parcel.writeInt(((isRollback)?(1):(0)));
+ _aidl_parcel.writeInt(rollbackId);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ public final void readFromParcel(android.os.Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ sessionId = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ childSessionIds = _aidl_parcel.createIntArray();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ hasRollbackEnabled = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ isRollback = (0!=_aidl_parcel.readInt());
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ rollbackId = _aidl_parcel.readInt();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ return _mask;
+ }
+}
diff --git a/android-34/android/apex/CompressedApexInfo.java b/android-34/android/apex/CompressedApexInfo.java
new file mode 100644
index 0000000..fa74887
--- /dev/null
+++ b/android-34/android/apex/CompressedApexInfo.java
@@ -0,0 +1,58 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package android.apex;
+public class CompressedApexInfo implements android.os.Parcelable
+{
+ public java.lang.String moduleName;
+ public long versionCode = 0L;
+ public long decompressedSize = 0L;
+ public static final android.os.Parcelable.Creator<CompressedApexInfo> CREATOR = new android.os.Parcelable.Creator<CompressedApexInfo>() {
+ @Override
+ public CompressedApexInfo createFromParcel(android.os.Parcel _aidl_source) {
+ CompressedApexInfo _aidl_out = new CompressedApexInfo();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public CompressedApexInfo[] newArray(int _aidl_size) {
+ return new CompressedApexInfo[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(android.os.Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeString(moduleName);
+ _aidl_parcel.writeLong(versionCode);
+ _aidl_parcel.writeLong(decompressedSize);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ public final void readFromParcel(android.os.Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ moduleName = _aidl_parcel.readString();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ versionCode = _aidl_parcel.readLong();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ decompressedSize = _aidl_parcel.readLong();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ return _mask;
+ }
+}
diff --git a/android-34/android/apex/CompressedApexInfoList.java b/android-34/android/apex/CompressedApexInfoList.java
new file mode 100644
index 0000000..94ed87e
--- /dev/null
+++ b/android-34/android/apex/CompressedApexInfoList.java
@@ -0,0 +1,65 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package android.apex;
+public class CompressedApexInfoList implements android.os.Parcelable
+{
+ public android.apex.CompressedApexInfo[] apexInfos;
+ public static final android.os.Parcelable.Creator<CompressedApexInfoList> CREATOR = new android.os.Parcelable.Creator<CompressedApexInfoList>() {
+ @Override
+ public CompressedApexInfoList createFromParcel(android.os.Parcel _aidl_source) {
+ CompressedApexInfoList _aidl_out = new CompressedApexInfoList();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public CompressedApexInfoList[] newArray(int _aidl_size) {
+ return new CompressedApexInfoList[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(android.os.Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeTypedArray(apexInfos, _aidl_flag);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ public final void readFromParcel(android.os.Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ apexInfos = _aidl_parcel.createTypedArray(android.apex.CompressedApexInfo.CREATOR);
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ _mask |= describeContents(apexInfos);
+ return _mask;
+ }
+ private int describeContents(Object _v) {
+ if (_v == null) return 0;
+ if (_v instanceof Object[]) {
+ int _mask = 0;
+ for (Object o : (Object[]) _v) {
+ _mask |= describeContents(o);
+ }
+ return _mask;
+ }
+ if (_v instanceof android.os.Parcelable) {
+ return ((android.os.Parcelable) _v).describeContents();
+ }
+ return 0;
+ }
+}
diff --git a/android-34/android/apex/IApexService.java b/android-34/android/apex/IApexService.java
new file mode 100644
index 0000000..4380430
--- /dev/null
+++ b/android-34/android/apex/IApexService.java
@@ -0,0 +1,1070 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package android.apex;
+public interface IApexService extends android.os.IInterface
+{
+ /** Default implementation for IApexService. */
+ public static class Default implements android.apex.IApexService
+ {
+ @Override public void submitStagedSession(android.apex.ApexSessionParams params, android.apex.ApexInfoList packages) throws android.os.RemoteException
+ {
+ }
+ @Override public void markStagedSessionReady(int session_id) throws android.os.RemoteException
+ {
+ }
+ @Override public void markStagedSessionSuccessful(int session_id) throws android.os.RemoteException
+ {
+ }
+ @Override public android.apex.ApexSessionInfo[] getSessions() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Override public android.apex.ApexSessionInfo getStagedSessionInfo(int session_id) throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Override public android.apex.ApexInfo[] getStagedApexInfos(android.apex.ApexSessionParams params) throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Override public android.apex.ApexInfo[] getActivePackages() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Override public android.apex.ApexInfo[] getAllPackages() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Override public void abortStagedSession(int session_id) throws android.os.RemoteException
+ {
+ }
+ @Override public void revertActiveSessions() throws android.os.RemoteException
+ {
+ }
+ /**
+ * Copies the CE apex data directory for the given user to the backup
+ * location.
+ */
+ @Override public void snapshotCeData(int user_id, int rollback_id, java.lang.String apex_name) throws android.os.RemoteException
+ {
+ }
+ /**
+ * Restores the snapshot of the CE apex data directory for the given user and
+ * apex. Note the snapshot will be deleted after restoration succeeded.
+ */
+ @Override public void restoreCeData(int user_id, int rollback_id, java.lang.String apex_name) throws android.os.RemoteException
+ {
+ }
+ /** Deletes device-encrypted snapshots for the given rollback id. */
+ @Override public void destroyDeSnapshots(int rollback_id) throws android.os.RemoteException
+ {
+ }
+ /** Deletes credential-encrypted snapshots for the given user, for the given rollback id. */
+ @Override public void destroyCeSnapshots(int user_id, int rollback_id) throws android.os.RemoteException
+ {
+ }
+ /**
+ * Deletes all credential-encrypted snapshots for the given user, except for
+ * those listed in retain_rollback_ids.
+ */
+ @Override public void destroyCeSnapshotsNotSpecified(int user_id, int[] retain_rollback_ids) throws android.os.RemoteException
+ {
+ }
+ @Override public void unstagePackages(java.util.List<java.lang.String> active_package_paths) throws android.os.RemoteException
+ {
+ }
+ /**
+ * Returns the active package corresponding to |package_name| and null
+ * if none exists.
+ */
+ @Override public android.apex.ApexInfo getActivePackage(java.lang.String package_name) throws android.os.RemoteException
+ {
+ return null;
+ }
+ /**
+ * Not meant for use outside of testing. The call will not be
+ * functional on user builds.
+ */
+ @Override public void stagePackages(java.util.List<java.lang.String> package_tmp_paths) throws android.os.RemoteException
+ {
+ }
+ /**
+ * Not meant for use outside of testing. The call will not be
+ * functional on user builds.
+ */
+ @Override public void resumeRevertIfNeeded() throws android.os.RemoteException
+ {
+ }
+ /**
+ * Forces apexd to remount all active packages.
+ *
+ * This call is mostly useful for speeding up development of APEXes.
+ * Instead of going through a full APEX installation that requires a reboot,
+ * developers can incorporate this method in much faster `adb sync` based
+ * workflow:
+ *
+ * 1. adb shell stop
+ * 2. adb sync
+ * 3. adb shell cmd -w apexservice remountPackages
+ * 4. adb shell start
+ *
+ * Note, that for an APEX package will be successfully remounted only if
+ * there are no alive processes holding a reference to it.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ @Override public void remountPackages() throws android.os.RemoteException
+ {
+ }
+ /**
+ * Forces apexd to recollect pre-installed data from the given |paths|.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ @Override public void recollectPreinstalledData(java.util.List<java.lang.String> paths) throws android.os.RemoteException
+ {
+ }
+ /**
+ * Forces apexd to recollect data apex from the given |path|.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ @Override public void recollectDataApex(java.lang.String path, java.lang.String decompression_dir) throws android.os.RemoteException
+ {
+ }
+ /** Informs apexd that the boot has completed. */
+ @Override public void markBootCompleted() throws android.os.RemoteException
+ {
+ }
+ /**
+ * Assuming the provided compressed APEX will be installed on next boot,
+ * calculate how much space will be required for decompression
+ */
+ @Override public long calculateSizeForCompressedApex(android.apex.CompressedApexInfoList compressed_apex_info_list) throws android.os.RemoteException
+ {
+ return 0L;
+ }
+ /**
+ * Reserve space on /data partition for compressed APEX decompression. Returns error if
+ * reservation fails. If empty list is passed, then reserved space is deallocated.
+ */
+ @Override public void reserveSpaceForCompressedApex(android.apex.CompressedApexInfoList compressed_apex_info_list) throws android.os.RemoteException
+ {
+ }
+ /**
+ * Performs a non-staged install of the given APEX.
+ * Note: don't confuse this to preInstall and postInstall binder calls which are only used to
+ * test corresponding features of APEX packages.
+ */
+ @Override public android.apex.ApexInfo installAndActivatePackage(java.lang.String packagePath) throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Override
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ public static abstract class Stub extends android.os.Binder implements android.apex.IApexService
+ {
+ /** Construct the stub at attach it to the interface. */
+ public Stub()
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+ /**
+ * Cast an IBinder object into an android.apex.IApexService interface,
+ * generating a proxy if needed.
+ */
+ public static android.apex.IApexService asInterface(android.os.IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof android.apex.IApexService))) {
+ return ((android.apex.IApexService)iin);
+ }
+ return new android.apex.IApexService.Stub.Proxy(obj);
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_submitStagedSession:
+ {
+ android.apex.ApexSessionParams _arg0;
+ _arg0 = data.readTypedObject(android.apex.ApexSessionParams.CREATOR);
+ android.apex.ApexInfoList _arg1;
+ _arg1 = new android.apex.ApexInfoList();
+ this.submitStagedSession(_arg0, _arg1);
+ reply.writeNoException();
+ reply.writeTypedObject(_arg1, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_markStagedSessionReady:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ this.markStagedSessionReady(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_markStagedSessionSuccessful:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ this.markStagedSessionSuccessful(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_getSessions:
+ {
+ android.apex.ApexSessionInfo[] _result = this.getSessions();
+ reply.writeNoException();
+ reply.writeTypedArray(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_getStagedSessionInfo:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ android.apex.ApexSessionInfo _result = this.getStagedSessionInfo(_arg0);
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_getStagedApexInfos:
+ {
+ android.apex.ApexSessionParams _arg0;
+ _arg0 = data.readTypedObject(android.apex.ApexSessionParams.CREATOR);
+ android.apex.ApexInfo[] _result = this.getStagedApexInfos(_arg0);
+ reply.writeNoException();
+ reply.writeTypedArray(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_getActivePackages:
+ {
+ android.apex.ApexInfo[] _result = this.getActivePackages();
+ reply.writeNoException();
+ reply.writeTypedArray(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_getAllPackages:
+ {
+ android.apex.ApexInfo[] _result = this.getAllPackages();
+ reply.writeNoException();
+ reply.writeTypedArray(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_abortStagedSession:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ this.abortStagedSession(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_revertActiveSessions:
+ {
+ this.revertActiveSessions();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_snapshotCeData:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ int _arg1;
+ _arg1 = data.readInt();
+ java.lang.String _arg2;
+ _arg2 = data.readString();
+ this.snapshotCeData(_arg0, _arg1, _arg2);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_restoreCeData:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ int _arg1;
+ _arg1 = data.readInt();
+ java.lang.String _arg2;
+ _arg2 = data.readString();
+ this.restoreCeData(_arg0, _arg1, _arg2);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_destroyDeSnapshots:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ this.destroyDeSnapshots(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_destroyCeSnapshots:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ int _arg1;
+ _arg1 = data.readInt();
+ this.destroyCeSnapshots(_arg0, _arg1);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_destroyCeSnapshotsNotSpecified:
+ {
+ int _arg0;
+ _arg0 = data.readInt();
+ int[] _arg1;
+ _arg1 = data.createIntArray();
+ this.destroyCeSnapshotsNotSpecified(_arg0, _arg1);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_unstagePackages:
+ {
+ java.util.List<java.lang.String> _arg0;
+ _arg0 = data.createStringArrayList();
+ this.unstagePackages(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_getActivePackage:
+ {
+ java.lang.String _arg0;
+ _arg0 = data.readString();
+ android.apex.ApexInfo _result = this.getActivePackage(_arg0);
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_stagePackages:
+ {
+ java.util.List<java.lang.String> _arg0;
+ _arg0 = data.createStringArrayList();
+ this.stagePackages(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_resumeRevertIfNeeded:
+ {
+ this.resumeRevertIfNeeded();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_remountPackages:
+ {
+ this.remountPackages();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_recollectPreinstalledData:
+ {
+ java.util.List<java.lang.String> _arg0;
+ _arg0 = data.createStringArrayList();
+ this.recollectPreinstalledData(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_recollectDataApex:
+ {
+ java.lang.String _arg0;
+ _arg0 = data.readString();
+ java.lang.String _arg1;
+ _arg1 = data.readString();
+ this.recollectDataApex(_arg0, _arg1);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_markBootCompleted:
+ {
+ this.markBootCompleted();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_calculateSizeForCompressedApex:
+ {
+ android.apex.CompressedApexInfoList _arg0;
+ _arg0 = data.readTypedObject(android.apex.CompressedApexInfoList.CREATOR);
+ long _result = this.calculateSizeForCompressedApex(_arg0);
+ reply.writeNoException();
+ reply.writeLong(_result);
+ break;
+ }
+ case TRANSACTION_reserveSpaceForCompressedApex:
+ {
+ android.apex.CompressedApexInfoList _arg0;
+ _arg0 = data.readTypedObject(android.apex.CompressedApexInfoList.CREATOR);
+ this.reserveSpaceForCompressedApex(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_installAndActivatePackage:
+ {
+ java.lang.String _arg0;
+ _arg0 = data.readString();
+ android.apex.ApexInfo _result = this.installAndActivatePackage(_arg0);
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements android.apex.IApexService
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public void submitStagedSession(android.apex.ApexSessionParams params, android.apex.ApexInfoList packages) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(params, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_submitStagedSession, _data, _reply, 0);
+ _reply.readException();
+ if ((0!=_reply.readInt())) {
+ packages.readFromParcel(_reply);
+ }
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void markStagedSessionReady(int session_id) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(session_id);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_markStagedSessionReady, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void markStagedSessionSuccessful(int session_id) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(session_id);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_markStagedSessionSuccessful, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public android.apex.ApexSessionInfo[] getSessions() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ android.apex.ApexSessionInfo[] _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getSessions, _data, _reply, 0);
+ _reply.readException();
+ _result = _reply.createTypedArray(android.apex.ApexSessionInfo.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public android.apex.ApexSessionInfo getStagedSessionInfo(int session_id) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ android.apex.ApexSessionInfo _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(session_id);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getStagedSessionInfo, _data, _reply, 0);
+ _reply.readException();
+ _result = _reply.readTypedObject(android.apex.ApexSessionInfo.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public android.apex.ApexInfo[] getStagedApexInfos(android.apex.ApexSessionParams params) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ android.apex.ApexInfo[] _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(params, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getStagedApexInfos, _data, _reply, 0);
+ _reply.readException();
+ _result = _reply.createTypedArray(android.apex.ApexInfo.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public android.apex.ApexInfo[] getActivePackages() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ android.apex.ApexInfo[] _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getActivePackages, _data, _reply, 0);
+ _reply.readException();
+ _result = _reply.createTypedArray(android.apex.ApexInfo.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public android.apex.ApexInfo[] getAllPackages() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ android.apex.ApexInfo[] _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getAllPackages, _data, _reply, 0);
+ _reply.readException();
+ _result = _reply.createTypedArray(android.apex.ApexInfo.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public void abortStagedSession(int session_id) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(session_id);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_abortStagedSession, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void revertActiveSessions() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_revertActiveSessions, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Copies the CE apex data directory for the given user to the backup
+ * location.
+ */
+ @Override public void snapshotCeData(int user_id, int rollback_id, java.lang.String apex_name) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(user_id);
+ _data.writeInt(rollback_id);
+ _data.writeString(apex_name);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_snapshotCeData, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Restores the snapshot of the CE apex data directory for the given user and
+ * apex. Note the snapshot will be deleted after restoration succeeded.
+ */
+ @Override public void restoreCeData(int user_id, int rollback_id, java.lang.String apex_name) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(user_id);
+ _data.writeInt(rollback_id);
+ _data.writeString(apex_name);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_restoreCeData, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /** Deletes device-encrypted snapshots for the given rollback id. */
+ @Override public void destroyDeSnapshots(int rollback_id) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(rollback_id);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_destroyDeSnapshots, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /** Deletes credential-encrypted snapshots for the given user, for the given rollback id. */
+ @Override public void destroyCeSnapshots(int user_id, int rollback_id) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(user_id);
+ _data.writeInt(rollback_id);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_destroyCeSnapshots, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Deletes all credential-encrypted snapshots for the given user, except for
+ * those listed in retain_rollback_ids.
+ */
+ @Override public void destroyCeSnapshotsNotSpecified(int user_id, int[] retain_rollback_ids) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeInt(user_id);
+ _data.writeIntArray(retain_rollback_ids);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_destroyCeSnapshotsNotSpecified, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void unstagePackages(java.util.List<java.lang.String> active_package_paths) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStringList(active_package_paths);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_unstagePackages, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Returns the active package corresponding to |package_name| and null
+ * if none exists.
+ */
+ @Override public android.apex.ApexInfo getActivePackage(java.lang.String package_name) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ android.apex.ApexInfo _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeString(package_name);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getActivePackage, _data, _reply, 0);
+ _reply.readException();
+ _result = _reply.readTypedObject(android.apex.ApexInfo.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ /**
+ * Not meant for use outside of testing. The call will not be
+ * functional on user builds.
+ */
+ @Override public void stagePackages(java.util.List<java.lang.String> package_tmp_paths) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStringList(package_tmp_paths);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_stagePackages, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Not meant for use outside of testing. The call will not be
+ * functional on user builds.
+ */
+ @Override public void resumeRevertIfNeeded() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_resumeRevertIfNeeded, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Forces apexd to remount all active packages.
+ *
+ * This call is mostly useful for speeding up development of APEXes.
+ * Instead of going through a full APEX installation that requires a reboot,
+ * developers can incorporate this method in much faster `adb sync` based
+ * workflow:
+ *
+ * 1. adb shell stop
+ * 2. adb sync
+ * 3. adb shell cmd -w apexservice remountPackages
+ * 4. adb shell start
+ *
+ * Note, that for an APEX package will be successfully remounted only if
+ * there are no alive processes holding a reference to it.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ @Override public void remountPackages() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_remountPackages, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Forces apexd to recollect pre-installed data from the given |paths|.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ @Override public void recollectPreinstalledData(java.util.List<java.lang.String> paths) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStringList(paths);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_recollectPreinstalledData, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Forces apexd to recollect data apex from the given |path|.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ @Override public void recollectDataApex(java.lang.String path, java.lang.String decompression_dir) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeString(path);
+ _data.writeString(decompression_dir);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_recollectDataApex, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /** Informs apexd that the boot has completed. */
+ @Override public void markBootCompleted() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_markBootCompleted, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Assuming the provided compressed APEX will be installed on next boot,
+ * calculate how much space will be required for decompression
+ */
+ @Override public long calculateSizeForCompressedApex(android.apex.CompressedApexInfoList compressed_apex_info_list) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ long _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(compressed_apex_info_list, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_calculateSizeForCompressedApex, _data, _reply, 0);
+ _reply.readException();
+ _result = _reply.readLong();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ /**
+ * Reserve space on /data partition for compressed APEX decompression. Returns error if
+ * reservation fails. If empty list is passed, then reserved space is deallocated.
+ */
+ @Override public void reserveSpaceForCompressedApex(android.apex.CompressedApexInfoList compressed_apex_info_list) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(compressed_apex_info_list, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_reserveSpaceForCompressedApex, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ /**
+ * Performs a non-staged install of the given APEX.
+ * Note: don't confuse this to preInstall and postInstall binder calls which are only used to
+ * test corresponding features of APEX packages.
+ */
+ @Override public android.apex.ApexInfo installAndActivatePackage(java.lang.String packagePath) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ android.apex.ApexInfo _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeString(packagePath);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_installAndActivatePackage, _data, _reply, 0);
+ _reply.readException();
+ _result = _reply.readTypedObject(android.apex.ApexInfo.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ }
+ static final int TRANSACTION_submitStagedSession = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_markStagedSessionReady = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ static final int TRANSACTION_markStagedSessionSuccessful = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+ static final int TRANSACTION_getSessions = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
+ static final int TRANSACTION_getStagedSessionInfo = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
+ static final int TRANSACTION_getStagedApexInfos = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5);
+ static final int TRANSACTION_getActivePackages = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6);
+ static final int TRANSACTION_getAllPackages = (android.os.IBinder.FIRST_CALL_TRANSACTION + 7);
+ static final int TRANSACTION_abortStagedSession = (android.os.IBinder.FIRST_CALL_TRANSACTION + 8);
+ static final int TRANSACTION_revertActiveSessions = (android.os.IBinder.FIRST_CALL_TRANSACTION + 9);
+ static final int TRANSACTION_snapshotCeData = (android.os.IBinder.FIRST_CALL_TRANSACTION + 10);
+ static final int TRANSACTION_restoreCeData = (android.os.IBinder.FIRST_CALL_TRANSACTION + 11);
+ static final int TRANSACTION_destroyDeSnapshots = (android.os.IBinder.FIRST_CALL_TRANSACTION + 12);
+ static final int TRANSACTION_destroyCeSnapshots = (android.os.IBinder.FIRST_CALL_TRANSACTION + 13);
+ static final int TRANSACTION_destroyCeSnapshotsNotSpecified = (android.os.IBinder.FIRST_CALL_TRANSACTION + 14);
+ static final int TRANSACTION_unstagePackages = (android.os.IBinder.FIRST_CALL_TRANSACTION + 15);
+ static final int TRANSACTION_getActivePackage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16);
+ static final int TRANSACTION_stagePackages = (android.os.IBinder.FIRST_CALL_TRANSACTION + 17);
+ static final int TRANSACTION_resumeRevertIfNeeded = (android.os.IBinder.FIRST_CALL_TRANSACTION + 18);
+ static final int TRANSACTION_remountPackages = (android.os.IBinder.FIRST_CALL_TRANSACTION + 19);
+ static final int TRANSACTION_recollectPreinstalledData = (android.os.IBinder.FIRST_CALL_TRANSACTION + 20);
+ static final int TRANSACTION_recollectDataApex = (android.os.IBinder.FIRST_CALL_TRANSACTION + 21);
+ static final int TRANSACTION_markBootCompleted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 22);
+ static final int TRANSACTION_calculateSizeForCompressedApex = (android.os.IBinder.FIRST_CALL_TRANSACTION + 23);
+ static final int TRANSACTION_reserveSpaceForCompressedApex = (android.os.IBinder.FIRST_CALL_TRANSACTION + 24);
+ static final int TRANSACTION_installAndActivatePackage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 25);
+ }
+ public static final java.lang.String DESCRIPTOR = "android$apex$IApexService".replace('$', '.');
+ public void submitStagedSession(android.apex.ApexSessionParams params, android.apex.ApexInfoList packages) throws android.os.RemoteException;
+ public void markStagedSessionReady(int session_id) throws android.os.RemoteException;
+ public void markStagedSessionSuccessful(int session_id) throws android.os.RemoteException;
+ public android.apex.ApexSessionInfo[] getSessions() throws android.os.RemoteException;
+ public android.apex.ApexSessionInfo getStagedSessionInfo(int session_id) throws android.os.RemoteException;
+ public android.apex.ApexInfo[] getStagedApexInfos(android.apex.ApexSessionParams params) throws android.os.RemoteException;
+ public android.apex.ApexInfo[] getActivePackages() throws android.os.RemoteException;
+ public android.apex.ApexInfo[] getAllPackages() throws android.os.RemoteException;
+ public void abortStagedSession(int session_id) throws android.os.RemoteException;
+ public void revertActiveSessions() throws android.os.RemoteException;
+ /**
+ * Copies the CE apex data directory for the given user to the backup
+ * location.
+ */
+ public void snapshotCeData(int user_id, int rollback_id, java.lang.String apex_name) throws android.os.RemoteException;
+ /**
+ * Restores the snapshot of the CE apex data directory for the given user and
+ * apex. Note the snapshot will be deleted after restoration succeeded.
+ */
+ public void restoreCeData(int user_id, int rollback_id, java.lang.String apex_name) throws android.os.RemoteException;
+ /** Deletes device-encrypted snapshots for the given rollback id. */
+ public void destroyDeSnapshots(int rollback_id) throws android.os.RemoteException;
+ /** Deletes credential-encrypted snapshots for the given user, for the given rollback id. */
+ public void destroyCeSnapshots(int user_id, int rollback_id) throws android.os.RemoteException;
+ /**
+ * Deletes all credential-encrypted snapshots for the given user, except for
+ * those listed in retain_rollback_ids.
+ */
+ public void destroyCeSnapshotsNotSpecified(int user_id, int[] retain_rollback_ids) throws android.os.RemoteException;
+ public void unstagePackages(java.util.List<java.lang.String> active_package_paths) throws android.os.RemoteException;
+ /**
+ * Returns the active package corresponding to |package_name| and null
+ * if none exists.
+ */
+ public android.apex.ApexInfo getActivePackage(java.lang.String package_name) throws android.os.RemoteException;
+ /**
+ * Not meant for use outside of testing. The call will not be
+ * functional on user builds.
+ */
+ public void stagePackages(java.util.List<java.lang.String> package_tmp_paths) throws android.os.RemoteException;
+ /**
+ * Not meant for use outside of testing. The call will not be
+ * functional on user builds.
+ */
+ public void resumeRevertIfNeeded() throws android.os.RemoteException;
+ /**
+ * Forces apexd to remount all active packages.
+ *
+ * This call is mostly useful for speeding up development of APEXes.
+ * Instead of going through a full APEX installation that requires a reboot,
+ * developers can incorporate this method in much faster `adb sync` based
+ * workflow:
+ *
+ * 1. adb shell stop
+ * 2. adb sync
+ * 3. adb shell cmd -w apexservice remountPackages
+ * 4. adb shell start
+ *
+ * Note, that for an APEX package will be successfully remounted only if
+ * there are no alive processes holding a reference to it.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ public void remountPackages() throws android.os.RemoteException;
+ /**
+ * Forces apexd to recollect pre-installed data from the given |paths|.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ public void recollectPreinstalledData(java.util.List<java.lang.String> paths) throws android.os.RemoteException;
+ /**
+ * Forces apexd to recollect data apex from the given |path|.
+ *
+ * Not meant for use outside of testing. This call will not be functional
+ * on user builds. Only root is allowed to call this method.
+ */
+ public void recollectDataApex(java.lang.String path, java.lang.String decompression_dir) throws android.os.RemoteException;
+ /** Informs apexd that the boot has completed. */
+ public void markBootCompleted() throws android.os.RemoteException;
+ /**
+ * Assuming the provided compressed APEX will be installed on next boot,
+ * calculate how much space will be required for decompression
+ */
+ public long calculateSizeForCompressedApex(android.apex.CompressedApexInfoList compressed_apex_info_list) throws android.os.RemoteException;
+ /**
+ * Reserve space on /data partition for compressed APEX decompression. Returns error if
+ * reservation fails. If empty list is passed, then reserved space is deallocated.
+ */
+ public void reserveSpaceForCompressedApex(android.apex.CompressedApexInfoList compressed_apex_info_list) throws android.os.RemoteException;
+ /**
+ * Performs a non-staged install of the given APEX.
+ * Note: don't confuse this to preInstall and postInstall binder calls which are only used to
+ * test corresponding features of APEX packages.
+ */
+ public android.apex.ApexInfo installAndActivatePackage(java.lang.String packagePath) throws android.os.RemoteException;
+}
diff --git a/android-34/android/app/EventLogTags.java b/android-34/android/app/EventLogTags.java
new file mode 100644
index 0000000..7af50a9
--- /dev/null
+++ b/android-34/android/app/EventLogTags.java
@@ -0,0 +1,82 @@
+/* This file is auto-generated. DO NOT MODIFY.
+ * Source file: frameworks/base/core/java/android/app/EventLogTags.logtags
+ */
+
+package android.app;
+
+/**
+ * @hide
+ */
+public class EventLogTags {
+ private EventLogTags() { } // don't instantiate
+
+ /** 30021 wm_on_paused_called (Token|1|5),(Component Name|3),(Reason|3),(time|2|3) */
+ public static final int WM_ON_PAUSED_CALLED = 30021;
+
+ /** 30022 wm_on_resume_called (Token|1|5),(Component Name|3),(Reason|3),(time|2|3) */
+ public static final int WM_ON_RESUME_CALLED = 30022;
+
+ /** 30049 wm_on_stop_called (Token|1|5),(Component Name|3),(Reason|3),(time|2|3) */
+ public static final int WM_ON_STOP_CALLED = 30049;
+
+ /** 30057 wm_on_create_called (Token|1|5),(Component Name|3),(Reason|3),(time|2|3) */
+ public static final int WM_ON_CREATE_CALLED = 30057;
+
+ /** 30058 wm_on_restart_called (Token|1|5),(Component Name|3),(Reason|3),(time|2|3) */
+ public static final int WM_ON_RESTART_CALLED = 30058;
+
+ /** 30059 wm_on_start_called (Token|1|5),(Component Name|3),(Reason|3),(time|2|3) */
+ public static final int WM_ON_START_CALLED = 30059;
+
+ /** 30060 wm_on_destroy_called (Token|1|5),(Component Name|3),(Reason|3),(time|2|3) */
+ public static final int WM_ON_DESTROY_CALLED = 30060;
+
+ /** 30062 wm_on_activity_result_called (Token|1|5),(Component Name|3),(Reason|3) */
+ public static final int WM_ON_ACTIVITY_RESULT_CALLED = 30062;
+
+ /** 30064 wm_on_top_resumed_gained_called (Token|1|5),(Component Name|3),(Reason|3) */
+ public static final int WM_ON_TOP_RESUMED_GAINED_CALLED = 30064;
+
+ /** 30065 wm_on_top_resumed_lost_called (Token|1|5),(Component Name|3),(Reason|3) */
+ public static final int WM_ON_TOP_RESUMED_LOST_CALLED = 30065;
+
+ public static void writeWmOnPausedCalled(int token, String componentName, String reason, long time) {
+ android.util.EventLog.writeEvent(WM_ON_PAUSED_CALLED, token, componentName, reason, time);
+ }
+
+ public static void writeWmOnResumeCalled(int token, String componentName, String reason, long time) {
+ android.util.EventLog.writeEvent(WM_ON_RESUME_CALLED, token, componentName, reason, time);
+ }
+
+ public static void writeWmOnStopCalled(int token, String componentName, String reason, long time) {
+ android.util.EventLog.writeEvent(WM_ON_STOP_CALLED, token, componentName, reason, time);
+ }
+
+ public static void writeWmOnCreateCalled(int token, String componentName, String reason, long time) {
+ android.util.EventLog.writeEvent(WM_ON_CREATE_CALLED, token, componentName, reason, time);
+ }
+
+ public static void writeWmOnRestartCalled(int token, String componentName, String reason, long time) {
+ android.util.EventLog.writeEvent(WM_ON_RESTART_CALLED, token, componentName, reason, time);
+ }
+
+ public static void writeWmOnStartCalled(int token, String componentName, String reason, long time) {
+ android.util.EventLog.writeEvent(WM_ON_START_CALLED, token, componentName, reason, time);
+ }
+
+ public static void writeWmOnDestroyCalled(int token, String componentName, String reason, long time) {
+ android.util.EventLog.writeEvent(WM_ON_DESTROY_CALLED, token, componentName, reason, time);
+ }
+
+ public static void writeWmOnActivityResultCalled(int token, String componentName, String reason) {
+ android.util.EventLog.writeEvent(WM_ON_ACTIVITY_RESULT_CALLED, token, componentName, reason);
+ }
+
+ public static void writeWmOnTopResumedGainedCalled(int token, String componentName, String reason) {
+ android.util.EventLog.writeEvent(WM_ON_TOP_RESUMED_GAINED_CALLED, token, componentName, reason);
+ }
+
+ public static void writeWmOnTopResumedLostCalled(int token, String componentName, String reason) {
+ android.util.EventLog.writeEvent(WM_ON_TOP_RESUMED_LOST_CALLED, token, componentName, reason);
+ }
+}
diff --git a/android-34/android/app/OverlayManagerPerfTest.java b/android-34/android/app/OverlayManagerPerfTest.java
deleted file mode 100644
index fcb13a8..0000000
--- a/android-34/android/app/OverlayManagerPerfTest.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app;
-
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.content.om.OverlayManager;
-import android.os.UserHandle;
-import android.perftests.utils.BenchmarkState;
-import android.perftests.utils.PerfStatusReporter;
-import android.perftests.utils.TestPackageInstaller;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.LargeTest;
-
-import com.android.perftests.core.R;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.concurrent.Executor;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Benchmarks for {@link android.content.om.OverlayManager}.
- */
-@LargeTest
-public class OverlayManagerPerfTest {
- private static final int OVERLAY_PKG_COUNT = 10;
- private static Context sContext;
- private static OverlayManager sOverlayManager;
- private static Executor sExecutor;
- private static ArrayList<TestPackageInstaller.InstalledPackage> sSmallOverlays =
- new ArrayList<>();
- private static ArrayList<TestPackageInstaller.InstalledPackage> sLargeOverlays =
- new ArrayList<>();
-
- @Rule
- public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
-
- @BeforeClass
- public static void classSetUp() throws Exception {
- sContext = InstrumentationRegistry.getTargetContext();
- sOverlayManager = new OverlayManager(sContext);
- sExecutor = (command) -> new Thread(command).start();
-
- // Install all of the test overlays.
- TestPackageInstaller installer = new TestPackageInstaller(sContext);
- for (int i = 0; i < OVERLAY_PKG_COUNT; i++) {
- sSmallOverlays.add(installer.installPackage("Overlay" + i +".apk"));
- sLargeOverlays.add(installer.installPackage("LargeOverlay" + i +".apk"));
- }
- }
-
- @AfterClass
- public static void classTearDown() throws Exception {
- for (TestPackageInstaller.InstalledPackage overlay : sSmallOverlays) {
- overlay.uninstall();
- }
-
- for (TestPackageInstaller.InstalledPackage overlay : sLargeOverlays) {
- overlay.uninstall();
- }
- }
-
- @After
- public void tearDown() throws Exception {
- // Disable all test overlays after each test.
- for (TestPackageInstaller.InstalledPackage overlay : sSmallOverlays) {
- assertSetEnabled(sContext, overlay.getPackageName(), false);
- }
-
- for (TestPackageInstaller.InstalledPackage overlay : sLargeOverlays) {
- assertSetEnabled(sContext, overlay.getPackageName(), false);
- }
- }
-
- /**
- * Enables the overlay and waits for the APK path change sto be propagated to the context
- * AssetManager.
- */
- private void assertSetEnabled(Context context, String overlayPackage, boolean eanabled)
- throws Exception {
- sOverlayManager.setEnabled(overlayPackage, true, UserHandle.SYSTEM);
-
- // Wait for the overlay changes to propagate
- FutureTask<Boolean> task = new FutureTask<>(() -> {
- while (true) {
- for (String path : context.getAssets().getApkPaths()) {
- if (eanabled == path.contains(overlayPackage)) {
- return true;
- }
- }
- }
- });
-
- sExecutor.execute(task);
- assertTrue("Failed to load overlay " + overlayPackage,
- task.get(20, TimeUnit.SECONDS));
- }
-
- @Test
- public void setEnabledWarmCache() throws Exception {
- String packageName = sSmallOverlays.get(0).getPackageName();
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- assertSetEnabled(sContext, packageName, true);
-
- // Disable the overlay for the next iteration of the test
- state.pauseTiming();
- assertSetEnabled(sContext, packageName, false);
- state.resumeTiming();
- }
- }
-
- @Test
- public void setEnabledColdCacheSmallOverlay() throws Exception {
- String packageName = sSmallOverlays.get(0).getPackageName();
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- assertSetEnabled(sContext, packageName, true);
-
- // Disable the overlay and remove the idmap for the next iteration of the test
- state.pauseTiming();
- assertSetEnabled(sContext, packageName, false);
- sOverlayManager.invalidateCachesForOverlay(packageName, UserHandle.SYSTEM);
- state.resumeTiming();
- }
- }
-
- @Test
- public void setEnabledColdCacheLargeOverlay() throws Exception {
- String packageName = sLargeOverlays.get(0).getPackageName();
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- assertSetEnabled(sContext, packageName, true);
-
- // Disable the overlay and remove the idmap for the next iteration of the test
- state.pauseTiming();
- assertSetEnabled(sContext, packageName, false);
- sOverlayManager.invalidateCachesForOverlay(packageName, UserHandle.SYSTEM);
- state.resumeTiming();
- }
- }
-
- @Test
- public void setEnabledDisable() throws Exception {
- String packageName = sSmallOverlays.get(0).getPackageName();
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- state.pauseTiming();
- assertSetEnabled(sContext, packageName, true);
- state.resumeTiming();
-
- assertSetEnabled(sContext, packageName, false);
- }
- }
-
- @Test
- public void getStringOneSmallOverlay() throws Exception {
- String packageName = sSmallOverlays.get(0).getPackageName();
- assertSetEnabled(sContext, packageName, true);
-
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- sContext.getString(R.string.short_text);
- }
-
- assertSetEnabled(sContext, packageName, false);
- }
-
- @Test
- public void getStringOneLargeOverlay() throws Exception {
- String packageName = sLargeOverlays.get(0).getPackageName();
- assertSetEnabled(sContext, packageName, true);
-
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- for (int resId = R.string.short_text000; resId < R.string.short_text255; resId++) {
- sContext.getString(resId);
- }
- }
-
- assertSetEnabled(sContext, packageName, false);
- }
-
- @Test
- public void getStringTenOverlays() throws Exception {
- // Enable all test overlays
- for (TestPackageInstaller.InstalledPackage overlay : sSmallOverlays) {
- assertSetEnabled(sContext, overlay.getPackageName(), true);
- }
-
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- sContext.getString(R.string.short_text);
- }
- }
-
- @Test
- public void getStringLargeTenOverlays() throws Exception {
- // Enable all test overlays
- for (TestPackageInstaller.InstalledPackage overlay : sLargeOverlays) {
- assertSetEnabled(sContext, overlay.getPackageName(), true);
- }
-
- BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- for (int resId = R.string.short_text000; resId < R.string.short_text255; resId++) {
- sContext.getString(resId);
- }
- }
- }
-}
diff --git a/android-34/android/app/PendingIntentPerfTest.java b/android-34/android/app/PendingIntentPerfTest.java
deleted file mode 100644
index 1bb98cb..0000000
--- a/android-34/android/app/PendingIntentPerfTest.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package android.app;
-
-import android.content.Context;
-import android.content.Intent;
-import android.perftests.utils.BenchmarkState;
-import android.perftests.utils.PerfStatusReporter;
-import android.perftests.utils.PerfTestActivity;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-// Due to b/71353150, you might get "java.lang.AssertionError: Binder ProxyMap has too many
-// entries", but it's flaky. Adding "Runtime.getRuntime().gc()" between each iteration solves
-// the problem, but it doesn't seem like it's currently needed.
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public class PendingIntentPerfTest {
-
- private Context mContext;
-
- @Rule
- public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
-
- private Intent mIntent;
-
- @Before
- public void setUp() {
- mContext = InstrumentationRegistry.getTargetContext();
- mIntent = PerfTestActivity.createLaunchIntent(mContext);
- }
-
- /**
- * Benchmark time to create a PendingIntent.
- */
- @Test
- public void create() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- state.pauseTiming();
- state.resumeTiming();
-
- final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
- PendingIntent.FLAG_MUTABLE_UNAUDITED);
-
- state.pauseTiming();
- pendingIntent.cancel();
- state.resumeTiming();
- }
- }
-
- /**
- * Benchmark time to create a PendingIntent with FLAG_CANCEL_CURRENT, already having an active
- * PendingIntent.
- */
- @Test
- public void createWithCancelFlag() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- state.pauseTiming();
- final PendingIntent previousPendingIntent = PendingIntent.getActivity(mContext, 0,
- mIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
- state.resumeTiming();
-
- final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
- PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
-
- state.pauseTiming();
- pendingIntent.cancel();
- state.resumeTiming();
- }
- }
-
- /**
- * Benchmark time to create a PendingIntent with FLAG_UPDATE_CURRENT, already having an active
- * PendingIntent.
- */
- @Test
- public void createWithUpdateFlag() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- state.pauseTiming();
- final PendingIntent previousPendingIntent = PendingIntent.getActivity(mContext, 0,
- mIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
- state.resumeTiming();
-
- final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
-
- state.pauseTiming();
- previousPendingIntent.cancel();
- pendingIntent.cancel();
- state.resumeTiming();
- }
- }
-
- /**
- * Benchmark time to cancel a PendingIntent.
- */
- @Test
- public void cancel() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- state.pauseTiming();
- final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0,
- mIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
- state.resumeTiming();
-
- pendingIntent.cancel();
- }
- }
-}
-
diff --git a/android-34/android/app/ResourcesManagerPerfTest.java b/android-34/android/app/ResourcesManagerPerfTest.java
deleted file mode 100644
index ac63653..0000000
--- a/android-34/android/app/ResourcesManagerPerfTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.app;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.perftests.utils.BenchmarkState;
-import android.perftests.utils.PerfStatusReporter;
-import android.view.Display;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.LargeTest;
-
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Benchmarks for {@link android.app.ResourcesManager}.
- */
-@LargeTest
-public class ResourcesManagerPerfTest {
- private static Context sContext;
- private static File sResourcesCompressed;
- private static File sResourcesUncompressed;
-
- @Rule
- public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
-
- @BeforeClass
- public static void setUp() throws Exception {
- sContext = InstrumentationRegistry.getTargetContext();
- sResourcesCompressed = copyApkToTemp("LargeResourcesCompressed.apk",
- "LargeResourcesCompressed.apk");
- sResourcesUncompressed = copyApkToTemp("LargeResourcesUncompressed.apk",
- "LargeResourcesUncompressed.apk");
- }
-
- @AfterClass
- public static void tearDown() {
- Assert.assertTrue(sResourcesCompressed.delete());
- Assert.assertTrue(sResourcesUncompressed.delete());
- }
-
- private static File copyApkToTemp(String inputFileName, String fileName) throws Exception {
- File file = File.createTempFile(fileName, null, sContext.getCacheDir());
- try (OutputStream tempOutputStream = new FileOutputStream(file);
- InputStream is = sContext.getResources().getAssets().openNonAsset(inputFileName)) {
- byte[] buffer = new byte[4096];
- int n;
- while ((n = is.read(buffer)) >= 0) {
- tempOutputStream.write(buffer, 0, n);
- }
- tempOutputStream.flush();
- }
- return file;
- }
-
- private void getResourcesForPath(String path) {
- ResourcesManager.getInstance().getResources(null, path, null, null, null, null,
- Display.DEFAULT_DISPLAY, null, sContext.getResources().getCompatibilityInfo(),
- null, null);
- }
-
- @Test
- public void getResourcesCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- getResourcesForPath(sResourcesCompressed.getPath());
- while (state.keepRunning()) {
- getResourcesForPath(sResourcesCompressed.getPath());
- }
- }
-
- @Test
- public void getResourcesCompressedUncached() {
- ResourcesManager resourcesManager = ResourcesManager.getInstance();
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- state.pauseTiming();
- resourcesManager.invalidatePath(sResourcesCompressed.getPath());
- state.resumeTiming();
-
- getResourcesForPath(sResourcesCompressed.getPath());
- }
- }
-
- @Test
- public void getResourcesUncompressedUncached() {
- ResourcesManager resourcesManager = ResourcesManager.getInstance();
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- state.pauseTiming();
- resourcesManager.invalidatePath(sResourcesUncompressed.getPath());
- state.resumeTiming();
-
- getResourcesForPath(sResourcesUncompressed.getPath());
- }
- }
-
- @Test
- public void applyConfigurationToResourcesLocked() {
- ResourcesManager resourcesManager = ResourcesManager.getInstance();
- Configuration c = new Configuration(resourcesManager.getConfiguration());
- c.uiMode = Configuration.UI_MODE_TYPE_WATCH;
-
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- resourcesManager.applyConfigurationToResources(c, null);
-
- // Alternate configurations to ensure the set configuration is different each iteration
- if (c.uiMode == Configuration.UI_MODE_TYPE_WATCH) {
- c.uiMode = Configuration.UI_MODE_TYPE_TELEVISION;
- } else {
- c.uiMode = Configuration.UI_MODE_TYPE_WATCH;
- }
- }
- }
-
- @Test
- public void getDisplayMetrics() {
- ResourcesManager resourcesManager = ResourcesManager.getInstance();
-
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- state.pauseTiming();
- // Invalidate cache.
- resourcesManager.applyConfigurationToResources(
- resourcesManager.getConfiguration(), null);
- state.resumeTiming();
-
- // Invoke twice for testing cache.
- resourcesManager.getDisplayMetrics();
- resourcesManager.getDisplayMetrics();
- }
- }
-}
diff --git a/android-34/android/app/ResourcesPerfTest.java b/android-34/android/app/ResourcesPerfTest.java
deleted file mode 100644
index 54b79b4..0000000
--- a/android-34/android/app/ResourcesPerfTest.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app;
-
-import static org.junit.Assert.fail;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.XmlResourceParser;
-import android.perftests.utils.BenchmarkState;
-import android.perftests.utils.PerfStatusReporter;
-import android.util.TypedValue;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.LargeTest;
-
-import com.android.perftests.core.R;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.Random;
-
-/**
- * Benchmarks for {@link android.content.res.Resources}.
- */
-@LargeTest
-public class ResourcesPerfTest {
- @Rule
- public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
-
- private Resources mRes;
-
- @Before
- public void setUp() {
- Context context = InstrumentationRegistry.getTargetContext();
- mRes = context.getResources();
- }
-
- @Test
- public void getValue() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- TypedValue value = new TypedValue();
- while (state.keepRunning()) {
- mRes.getValue(R.integer.forty_two, value, false /* resolve_refs */);
- }
- }
-
- @Test
- public void getFrameworkValue() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- TypedValue value = new TypedValue();
- while (state.keepRunning()) {
- mRes.getValue(com.android.internal.R.integer.autofill_max_visible_datasets, value,
- false /* resolve_refs */);
- }
- }
-
- @Test
- public void getValueString() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- TypedValue value = new TypedValue();
- while (state.keepRunning()) {
- mRes.getValue(R.string.long_text, value, false /* resolve_refs */);
- }
- }
-
- @Test
- public void getFrameworkStringValue() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- TypedValue value = new TypedValue();
- while (state.keepRunning()) {
- mRes.getValue(com.android.internal.R.string.cancel, value, false /* resolve_refs */);
- }
- }
-
- @Test
- public void getValueManyConfigurations() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- TypedValue value = new TypedValue();
- while (state.keepRunning()) {
- mRes.getValue(com.android.internal.R.string.mmcc_illegal_me, value,
- false /* resolve_refs */);
- }
- }
-
- @Test
- public void getText() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getText(R.string.long_text);
- }
- }
-
-
- @Test
- public void getFont() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getFont(R.font.samplefont);
- }
- }
-
- @Test
- public void getString() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getString(R.string.long_text);
- }
- }
-
- @Test
- public void getQuantityString() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getQuantityString(R.plurals.plurals_text, 5);
- }
- }
-
- @Test
- public void getQuantityText() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getQuantityText(R.plurals.plurals_text, 5);
- }
- }
-
- @Test
- public void getTextArray() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getTextArray(R.array.strings);
- }
- }
-
- @Test
- public void getStringArray() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getStringArray(R.array.strings);
- }
- }
-
- @Test
- public void getIntegerArray() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getIntArray(R.array.ints);
- }
- }
-
- @Test
- public void getColor() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getColor(R.color.white, null);
- }
- }
-
- @Test
- public void getColorStateList() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getColorStateList(R.color.color_state_list, null);
- }
- }
-
- @Test
- public void getVectorDrawable() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mRes.getDrawable(R.drawable.vector_drawable01, null);
- }
- }
-
- @Test
- public void getLayoutAndTravese() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- try (XmlResourceParser parser = mRes.getLayout(R.layout.test_relative_layout)) {
- while (parser.next() != XmlPullParser.END_DOCUMENT) {
- // Walk the entire tree
- }
- } catch (IOException | XmlPullParserException exception) {
- fail("Parsing of the layout failed. Something is really broken");
- }
- }
- }
-
- @Test
- public void getLayoutAndTraverseInvalidateCaches() {
- mRes.flushLayoutCache();
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- try (XmlResourceParser parser = mRes.getLayout(R.layout.test_relative_layout)) {
- while (parser.next() != XmlPullParser.END_DOCUMENT) {
- // Walk the entire tree
- }
- } catch (IOException | XmlPullParserException exception) {
- fail("Parsing of the layout failed. Something is really broken");
- }
-
- state.pauseTiming();
- mRes.flushLayoutCache();
- state.resumeTiming();
- }
- }
-
- @Test
- public void getIdentifier() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- final Random random = new Random(System.currentTimeMillis());
- final Context context = InstrumentationRegistry.getTargetContext();
- final String packageName = context.getPackageName();
- while (state.keepRunning()) {
- state.pauseTiming();
- final int expectedInteger = random.nextInt(10001);
- final String expectedString = Integer.toHexString(expectedInteger);
- final String entryName = "i_am_color_" + expectedString;
- state.resumeTiming();
-
- final int resIdentifier = mRes.getIdentifier(entryName, "color", packageName);
- if (resIdentifier == 0) {
- fail("Color \"" + entryName + "\" is not found");
- }
- }
- }
-}
diff --git a/android-34/android/app/ResourcesThemePerfTest.java b/android-34/android/app/ResourcesThemePerfTest.java
deleted file mode 100644
index 45c723b..0000000
--- a/android-34/android/app/ResourcesThemePerfTest.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.app;
-
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.UserHandle;
-import android.perftests.utils.BenchmarkState;
-import android.perftests.utils.PerfStatusReporter;
-import android.view.Display;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.LargeTest;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-/**
- * Benchmarks for {@link android.content.res.Resources.Theme}.
- */
-@LargeTest
-public class ResourcesThemePerfTest {
- @Rule
- public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
-
- private Context mContext;
- private int mThemeResId;
- private Resources.Theme mTheme;
-
- @Before
- public void setUp() throws Exception {
- mContext = InstrumentationRegistry.getTargetContext();
- mThemeResId = com.android.perftests.core.R.style.Base_V7_Theme_AppCompat;
- mTheme = mContext.getResources().newTheme();
- mTheme.applyStyle(mThemeResId, true /* force */);
- }
-
- @Test
- public void applyStyle() {
- Resources.Theme destTheme = mContext.getResources().newTheme();
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- destTheme.applyStyle(mThemeResId, true /* force */);
- }
- }
-
- @Test
- public void rebase() {
- Resources.Theme destTheme = mContext.getResources().newTheme();
- destTheme.applyStyle(mThemeResId, true /* force */);
- destTheme.applyStyle(android.R.style.Theme_Material, true /* force */);
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- destTheme.rebase();
- }
- }
-
- @Test
- public void setToSameAssetManager() {
- Resources.Theme destTheme = mContext.getResources().newTheme();
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- destTheme.setTo(mTheme);
- }
- }
-
- @Test
- public void setToDifferentAssetManager() throws Exception {
- // Create a new Resources object with the same asset paths but a different AssetManager
- PackageManager packageManager = mContext.getApplicationContext().getPackageManager();
- ApplicationInfo ai = packageManager.getApplicationInfo(mContext.getPackageName(),
- UserHandle.myUserId());
-
- ResourcesManager resourcesManager = ResourcesManager.getInstance();
- Configuration c = resourcesManager.getConfiguration();
- c.orientation = (c.orientation == Configuration.ORIENTATION_PORTRAIT)
- ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT;
-
- Resources destResources = resourcesManager.getResources(null, ai.sourceDir,
- ai.splitSourceDirs, ai.resourceDirs, ai.overlayPaths, ai.sharedLibraryFiles,
- Display.DEFAULT_DISPLAY, c, mContext.getResources().getCompatibilityInfo(),
- null, null);
- Assert.assertNotEquals(destResources.getAssets(), mContext.getAssets());
-
- Resources.Theme destTheme = destResources.newTheme();
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- destTheme.setTo(mTheme);
- }
- }
-
- @Test
- public void obtainStyledAttributesForViewFromMaterial() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- mTheme.obtainStyledAttributes(android.R.style.Theme_Material, android.R.styleable.View);
- }
- }
-}
\ No newline at end of file
diff --git a/android-34/android/app/StatsCursor.java b/android-34/android/app/StatsCursor.java
new file mode 100644
index 0000000..29cd241
--- /dev/null
+++ b/android-34/android/app/StatsCursor.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.SuppressLint;
+import android.database.AbstractCursor;
+import android.database.MatrixCursor;
+
+/**
+ * Custom cursor implementation to hold a cross-process cursor to pass data to caller.
+ *
+ * @hide
+ */
+@SystemApi
+public class StatsCursor extends AbstractCursor {
+ private final MatrixCursor mMatrixCursor;
+ private final int[] mColumnTypes;
+ private final String[] mColumnNames;
+ private final int mRowCount;
+
+ /**
+ * @hide
+ **/
+ public StatsCursor(String[] queryData, String[] columnNames, int[] columnTypes, int rowCount) {
+ mColumnTypes = columnTypes;
+ mColumnNames = columnNames;
+ mRowCount = rowCount;
+ mMatrixCursor = new MatrixCursor(columnNames);
+ for (int i = 0; i < rowCount; i++) {
+ MatrixCursor.RowBuilder builder = mMatrixCursor.newRow();
+ for (int j = 0; j < columnNames.length; j++) {
+ int dataIndex = i * columnNames.length + j;
+ builder.add(columnNames[j], queryData[dataIndex]);
+ }
+ }
+ }
+
+ /**
+ * Returns the numbers of rows in the cursor.
+ *
+ * @return the number of rows in the cursor.
+ */
+ @Override
+ public int getCount() {
+ return mRowCount;
+ }
+
+ /**
+ * Returns a string array holding the names of all of the columns in the
+ * result set in the order in which they were listed in the result.
+ *
+ * @return the names of the columns returned in this query.
+ */
+ @Override
+ @NonNull
+ public String[] getColumnNames() {
+ return mColumnNames;
+ }
+
+ /**
+ * Returns the value of the requested column as a String.
+ *
+ * @param column the zero-based index of the target column.
+ * @return the value of that column as a String.
+ */
+ @Override
+ @NonNull
+ public String getString(int column) {
+ return mMatrixCursor.getString(column);
+ }
+
+ /**
+ * Returns the value of the requested column as a short.
+ *
+ * @param column the zero-based index of the target column.
+ * @return the value of that column as a short.
+ */
+ @Override
+ @SuppressLint("NoByteOrShort")
+ public short getShort(int column) {
+ return mMatrixCursor.getShort(column);
+ }
+
+ /**
+ * Returns the value of the requested column as an int.
+ *
+ * @param column the zero-based index of the target column.
+ * @return the value of that column as an int.
+ */
+ @Override
+ public int getInt(int column) {
+ return mMatrixCursor.getInt(column);
+ }
+
+ /**
+ * Returns the value of the requested column as a long.
+ *
+ * @param column the zero-based index of the target column.
+ * @return the value of that column as a long.
+ */
+ @Override
+ public long getLong(int column) {
+ return mMatrixCursor.getLong(column);
+ }
+
+ /**
+ * Returns the value of the requested column as a float.
+ *
+ * @param column the zero-based index of the target column.
+ * @return the value of that column as a float.
+ */
+ @Override
+ public float getFloat(int column) {
+ return mMatrixCursor.getFloat(column);
+ }
+
+ /**
+ * Returns the value of the requested column as a double.
+ *
+ * @param column the zero-based index of the target column.
+ * @return the value of that column as a double.
+ */
+ @Override
+ public double getDouble(int column) {
+ return mMatrixCursor.getDouble(column);
+ }
+
+ /**
+ * Returns <code>true</code> if the value in the indicated column is null.
+ *
+ * @param column the zero-based index of the target column.
+ * @return whether the column value is null.
+ */
+ @Override
+ public boolean isNull(int column) {
+ return mMatrixCursor.isNull(column);
+ }
+
+ /**
+ * Returns the data type of the given column's value.
+ *
+ * @param column the zero-based index of the target column.
+ * @return column value type
+ */
+ @Override
+ public int getType(int column) {
+ return mColumnTypes[column];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ return mMatrixCursor.moveToPosition(newPosition);
+ }
+}
diff --git a/android-34/android/app/StatsManager.java b/android-34/android/app/StatsManager.java
new file mode 100644
index 0000000..a0379fd
--- /dev/null
+++ b/android-34/android/app/StatsManager.java
@@ -0,0 +1,919 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.PACKAGE_USAGE_STATS;
+import static android.Manifest.permission.READ_RESTRICTED_STATS;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IPullAtomCallback;
+import android.os.IPullAtomResultReceiver;
+import android.os.IStatsManagerService;
+import android.os.IStatsQueryCallback;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.StatsFrameworkInitializer;
+import android.util.AndroidException;
+import android.util.Log;
+import android.util.StatsEvent;
+import android.util.StatsEventParcel;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * API for statsd clients to send configurations and retrieve data.
+ *
+ * @hide
+ */
+@SystemApi
+public final class StatsManager {
+ private static final String TAG = "StatsManager";
+ private static final boolean DEBUG = false;
+
+ private static final Object sLock = new Object();
+ private final Context mContext;
+
+ @GuardedBy("sLock")
+ private IStatsManagerService mStatsManagerService;
+
+ /**
+ * Long extra of uid that added the relevant stats config.
+ */
+ public static final String EXTRA_STATS_CONFIG_UID = "android.app.extra.STATS_CONFIG_UID";
+ /**
+ * Long extra of the relevant stats config's configKey.
+ */
+ public static final String EXTRA_STATS_CONFIG_KEY = "android.app.extra.STATS_CONFIG_KEY";
+ /**
+ * Long extra of the relevant statsd_config.proto's Subscription.id.
+ */
+ public static final String EXTRA_STATS_SUBSCRIPTION_ID =
+ "android.app.extra.STATS_SUBSCRIPTION_ID";
+ /**
+ * Long extra of the relevant statsd_config.proto's Subscription.rule_id.
+ */
+ public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID =
+ "android.app.extra.STATS_SUBSCRIPTION_RULE_ID";
+ /**
+ * List<String> of the relevant statsd_config.proto's BroadcastSubscriberDetails.cookie.
+ * Obtain using {@link android.content.Intent#getStringArrayListExtra(String)}.
+ */
+ public static final String EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES =
+ "android.app.extra.STATS_BROADCAST_SUBSCRIBER_COOKIES";
+ /**
+ * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value
+ * information.
+ */
+ public static final String EXTRA_STATS_DIMENSIONS_VALUE =
+ "android.app.extra.STATS_DIMENSIONS_VALUE";
+ /**
+ * Long array extra of the active configs for the uid that added those configs.
+ */
+ public static final String EXTRA_STATS_ACTIVE_CONFIG_KEYS =
+ "android.app.extra.STATS_ACTIVE_CONFIG_KEYS";
+
+ /**
+ * Long array extra of the restricted metric ids present for the client.
+ */
+ public static final String EXTRA_STATS_RESTRICTED_METRIC_IDS =
+ "android.app.extra.STATS_RESTRICTED_METRIC_IDS";
+
+ /**
+ * Broadcast Action: Statsd has started.
+ * Configurations and PendingIntents can now be sent to it.
+ */
+ public static final String ACTION_STATSD_STARTED = "android.app.action.STATSD_STARTED";
+
+ // Pull atom callback return codes.
+ /**
+ * Value indicating that this pull was successful and that the result should be used.
+ *
+ **/
+ public static final int PULL_SUCCESS = 0;
+
+ /**
+ * Value indicating that this pull was unsuccessful and that the result should not be used.
+ **/
+ public static final int PULL_SKIP = 1;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting public static final long DEFAULT_COOL_DOWN_MILLIS = 1_000L; // 1 second.
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting public static final long DEFAULT_TIMEOUT_MILLIS = 1_500L; // 1.5 seconds.
+
+ /**
+ * Constructor for StatsManagerClient.
+ *
+ * @hide
+ */
+ public StatsManager(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Adds the given configuration and associates it with the given configKey. If a config with the
+ * given configKey already exists for the caller's uid, it is replaced with the new one.
+ * This call can block on statsd.
+ *
+ * @param configKey An arbitrary integer that allows clients to track the configuration.
+ * @param config Wire-encoded StatsdConfig proto that specifies metrics (and all
+ * dependencies eg, conditions and matchers).
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public void addConfig(long configKey, byte[] config) throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ // can throw IllegalArgumentException
+ service.addConfiguration(configKey, config, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when adding configuration");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to addConfig in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #addConfig(long, byte[])}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public boolean addConfiguration(long configKey, byte[] config) {
+ try {
+ addConfig(configKey, config);
+ return true;
+ } catch (StatsUnavailableException | IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Remove a configuration from logging.
+ *
+ * This call can block on statsd.
+ *
+ * @param configKey Configuration key to remove.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public void removeConfig(long configKey) throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ service.removeConfiguration(configKey, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when removing configuration");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to removeConfig in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #removeConfig(long)}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public boolean removeConfiguration(long configKey) {
+ try {
+ removeConfig(configKey);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Set the PendingIntent to be used when broadcasting subscriber information to the given
+ * subscriberId within the given config.
+ * <p>
+ * Suppose that the calling uid has added a config with key configKey, and that in this config
+ * it is specified that when a particular anomaly is detected, a broadcast should be sent to
+ * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
+ * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
+ * when the anomaly is detected.
+ * <p>
+ * When statsd sends the broadcast, the PendingIntent will used to send an intent with
+ * information of
+ * {@link #EXTRA_STATS_CONFIG_UID},
+ * {@link #EXTRA_STATS_CONFIG_KEY},
+ * {@link #EXTRA_STATS_SUBSCRIPTION_ID},
+ * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID},
+ * {@link #EXTRA_STATS_BROADCAST_SUBSCRIBER_COOKIES}, and
+ * {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
+ * <p>
+ * This function can only be called by the owner (uid) of the config. It must be called each
+ * time statsd starts. The config must have been added first (via {@link #addConfig}).
+ * This call can block on statsd.
+ *
+ * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
+ * associated with the given subscriberId. May be null, in which case
+ * it undoes any previous setting of this subscriberId.
+ * @param configKey The integer naming the config to which this subscriber is attached.
+ * @param subscriberId ID of the subscriber, as used in the config.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public void setBroadcastSubscriber(
+ PendingIntent pendingIntent, long configKey, long subscriberId)
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ if (pendingIntent != null) {
+ service.setBroadcastSubscriber(configKey, subscriberId, pendingIntent,
+ mContext.getOpPackageName());
+ } else {
+ service.unsetBroadcastSubscriber(configKey, subscriberId,
+ mContext.getOpPackageName());
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when adding broadcast subscriber",
+ e);
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #setBroadcastSubscriber(PendingIntent, long, long)}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public boolean setBroadcastSubscriber(
+ long configKey, long subscriberId, PendingIntent pendingIntent) {
+ try {
+ setBroadcastSubscriber(pendingIntent, configKey, subscriberId);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Registers the operation that is called to retrieve the metrics data. This must be called
+ * each time statsd starts. The config must have been added first (via {@link #addConfig},
+ * although addConfig could have been called on a previous boot). This operation allows
+ * statsd to send metrics data whenever statsd determines that the metrics in memory are
+ * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch
+ * the data, which also deletes the retrieved metrics from statsd's memory.
+ * This call can block on statsd.
+ *
+ * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
+ * associated with the given subscriberId. May be null, in which case
+ * it removes any associated pending intent with this configKey.
+ * @param configKey The integer naming the config to which this operation is attached.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey)
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ if (pendingIntent == null) {
+ service.removeDataFetchOperation(configKey, mContext.getOpPackageName());
+ } else {
+ service.setDataFetchOperation(configKey, pendingIntent,
+ mContext.getOpPackageName());
+ }
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when registering data listener.");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Registers the operation that is called whenever there is a change in which configs are
+ * active. This must be called each time statsd starts. This operation allows
+ * statsd to inform clients that they should pull data of the configs that are currently
+ * active. The activeConfigsChangedOperation should set periodic alarms to pull data of configs
+ * that are active and stop pulling data of configs that are no longer active.
+ * This call can block on statsd.
+ *
+ * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
+ * associated with the given subscriberId. May be null, in which case
+ * it removes any associated pending intent for this client.
+ * @return A list of configs that are currently active for this client. If the pendingIntent is
+ * null, this will be an empty list.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public @NonNull long[] setActiveConfigsChangedOperation(@Nullable PendingIntent pendingIntent)
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ if (pendingIntent == null) {
+ service.removeActiveConfigsChangedOperation(mContext.getOpPackageName());
+ return new long[0];
+ } else {
+ return service.setActiveConfigsChangedOperation(pendingIntent,
+ mContext.getOpPackageName());
+ }
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager "
+ + "when registering active configs listener.");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Registers the operation that is called whenever there is a change in the restricted metrics
+ * for a specified config that are present for this client. This operation allows statsd to
+ * inform the client about the current restricted metric ids available to be queried for the
+ * specified config. This call can block on statsd.
+ *
+ * If there is no config in statsd that matches the provided config package and key, an empty
+ * list is returned. The pending intent will be tracked, and the operation will be called
+ * whenever a matching config is added.
+ *
+ * @param configKey The configKey passed by the package that added the config in
+ * StatsManager#addConfig
+ * @param configPackage The package that added the config in StatsManager#addConfig
+ * @param pendingIntent the PendingIntent to use when broadcasting info to caller.
+ * May be null, in which case it removes any associated pending intent
+ * for this client.
+ * @return A list of metric ids identifying the restricted metrics that are currently available
+ * to be queried for the specified config.
+ * If the pendingIntent is null, this will be an empty list.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(READ_RESTRICTED_STATS)
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public @NonNull long[] setRestrictedMetricsChangedOperation(long configKey,
+ @NonNull String configPackage,
+ @Nullable PendingIntent pendingIntent)
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ if (pendingIntent == null) {
+ service.removeRestrictedMetricsChangedOperation(configKey, configPackage);
+ return new long[0];
+ } else {
+ return service.setRestrictedMetricsChangedOperation(pendingIntent,
+ configKey, configPackage);
+ }
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager "
+ + "when registering restricted metrics listener.");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Queries the underlying service based on query received and populates the OutcomeReceiver via
+ * callback. This call is blocking on statsd being available, but is otherwise nonblocking.
+ * i.e. the call can return before the query processing is done.
+ * <p>
+ * Two types of tables are supported: Metric tables and the device information table.
+ * </p>
+ * <p>
+ * The device information table is named device_info and contains the following columns:
+ * sdkVersion, model, product, hardware, device, osBuild, fingerprint, brand, manufacturer, and
+ * board. These columns correspond to {@link Build.VERSION.SDK_INT}, {@link Build.MODEL},
+ * {@link Build.PRODUCT}, {@link Build.HARDWARE}, {@link Build.DEVICE}, {@link Build.ID},
+ * {@link Build.FINGERPRINT}, {@link Build.BRAND}, {@link Build.MANUFACTURER},
+ * {@link Build.BOARD} respectively.
+ * </p>
+ * <p>
+ * The metric tables are named metric_METRIC_ID where METRIC_ID is the metric id that is part
+ * of the wire encoded config passed to {@link #addConfig(long, byte[])}. If the metric id is
+ * negative, then the '-' character is replaced with 'n' in the table name. Each metric table
+ * contains the 3 columns followed by n columns of the following form: atomId,
+ * elapsedTimestampNs, wallTimestampNs, field_1, field_2, field_3 ... field_n. These
+ * columns correspond to to the id of the atom from frameworks/proto_logging/stats/atoms.proto,
+ * time when the atom is recorded, and the data fields within each atom.
+ * </p>
+ * @param configKey The configKey passed by the package that added
+ * the config being queried in StatsManager#addConfig
+ * @param configPackage The package that added the config being queried in
+ * StatsManager#addConfig
+ * @param query the query object encapsulating a sql-string and necessary config to query
+ * underlying sql-based data store.
+ * @param executor the executor on which outcomeReceiver will be invoked.
+ * @param outcomeReceiver the receiver to be populated with cursor pointing to result data.
+ */
+ @RequiresPermission(READ_RESTRICTED_STATS)
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void query(long configKey, @NonNull String configPackage, @NonNull StatsQuery query,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<StatsCursor, StatsQueryException> outcomeReceiver)
+ throws StatsUnavailableException {
+ if(query.getSqlDialect() != StatsQuery.DIALECT_SQLITE) {
+ executor.execute(() -> {
+ outcomeReceiver.onError(new StatsQueryException("Unsupported Sql Dialect"));
+ });
+ return;
+ }
+
+ StatsQueryCallbackInternal callbackInternal =
+ new StatsQueryCallbackInternal(outcomeReceiver, executor);
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ service.querySql(query.getRawSql(), query.getMinSqlClientVersion(),
+ query.getPolicyConfig(), callbackInternal, configKey,
+ configPackage);
+ } catch (RemoteException | IllegalStateException e) {
+ throw new StatsUnavailableException("could not connect", e);
+ }
+ }
+ }
+
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #setFetchReportsOperation(PendingIntent, long)}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
+ try {
+ setFetchReportsOperation(pendingIntent, configKey);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Request the data collected for the given configKey.
+ * This getter is destructive - it also clears the retrieved metrics from statsd's memory.
+ * This call can block on statsd.
+ *
+ * @param configKey Configuration key to retrieve data from.
+ * @return Serialized ConfigMetricsReportList proto.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public byte[] getReports(long configKey) throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ return service.getData(configKey, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when getting data");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to getReports in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #getReports(long)}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public @Nullable byte[] getData(long configKey) {
+ try {
+ return getReports(configKey);
+ } catch (StatsUnavailableException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Clients can request metadata for statsd. Will contain stats across all configurations but not
+ * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
+ * This getter is not destructive and will not reset any metrics/counters.
+ * This call can block on statsd.
+ *
+ * @return Serialized StatsdStatsReport proto.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public byte[] getStatsMetadata() throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ return service.getMetadata(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to connect to statsmanager when getting metadata");
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to getStatsMetadata in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // TODO: Temporary for backwards compatibility. Remove.
+ /**
+ * @deprecated Use {@link #getStatsMetadata()}
+ */
+ @Deprecated
+ @RequiresPermission(allOf = { DUMP, PACKAGE_USAGE_STATS })
+ public @Nullable byte[] getMetadata() {
+ try {
+ return getStatsMetadata();
+ } catch (StatsUnavailableException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the experiments IDs registered with statsd, or an empty array if there aren't any.
+ *
+ * This call can block on statsd.
+ *
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ */
+ @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS})
+ public long[] getRegisteredExperimentIds()
+ throws StatsUnavailableException {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ return service.getRegisteredExperimentIds();
+ } catch (RemoteException e) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "Failed to connect to StatsManagerService when getting "
+ + "registered experiment IDs");
+ }
+ throw new StatsUnavailableException("could not connect", e);
+ } catch (SecurityException e) {
+ throw new StatsUnavailableException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to getRegisteredExperimentIds in statsmanager");
+ throw new StatsUnavailableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Sets a callback for an atom when that atom is to be pulled. The stats service will
+ * invoke pullData in the callback when the stats service determines that this atom needs to be
+ * pulled. This method should not be called by third-party apps.
+ *
+ * @param atomTag The tag of the atom for this puller callback.
+ * @param metadata Optional metadata specifying the timeout, cool down time, and
+ * additive fields for mapping isolated to host uids.
+ * @param executor The executor in which to run the callback.
+ * @param callback The callback to be invoked when the stats service pulls the atom.
+ *
+ */
+ @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM)
+ public void setPullAtomCallback(int atomTag, @Nullable PullAtomMetadata metadata,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull StatsPullAtomCallback callback) {
+ long coolDownMillis =
+ metadata == null ? DEFAULT_COOL_DOWN_MILLIS : metadata.mCoolDownMillis;
+ long timeoutMillis = metadata == null ? DEFAULT_TIMEOUT_MILLIS : metadata.mTimeoutMillis;
+ int[] additiveFields = metadata == null ? new int[0] : metadata.mAdditiveFields;
+ if (additiveFields == null) {
+ additiveFields = new int[0];
+ }
+
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ PullAtomCallbackInternal rec =
+ new PullAtomCallbackInternal(atomTag, callback, executor);
+ service.registerPullAtomCallback(
+ atomTag, coolDownMillis, timeoutMillis, additiveFields, rec);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Unable to register pull callback", e);
+ }
+ }
+ }
+
+ /**
+ * Clears a callback for an atom when that atom is to be pulled. Note that any ongoing
+ * pulls will still occur. This method should not be called by third-party apps.
+ *
+ * @param atomTag The tag of the atom of which to unregister
+ *
+ */
+ @RequiresPermission(android.Manifest.permission.REGISTER_STATS_PULL_ATOM)
+ public void clearPullAtomCallback(int atomTag) {
+ synchronized (sLock) {
+ try {
+ IStatsManagerService service = getIStatsManagerServiceLocked();
+ service.unregisterPullAtomCallback(atomTag);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Unable to unregister pull atom callback");
+ }
+ }
+ }
+
+ private static class PullAtomCallbackInternal extends IPullAtomCallback.Stub {
+ public final int mAtomId;
+ public final StatsPullAtomCallback mCallback;
+ public final Executor mExecutor;
+
+ PullAtomCallbackInternal(int atomId, StatsPullAtomCallback callback, Executor executor) {
+ mAtomId = atomId;
+ mCallback = callback;
+ mExecutor = executor;
+ }
+
+ @Override
+ public void onPullAtom(int atomTag, IPullAtomResultReceiver resultReceiver) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ List<StatsEvent> data = new ArrayList<>();
+ int successInt = mCallback.onPullAtom(atomTag, data);
+ boolean success = successInt == PULL_SUCCESS;
+ StatsEventParcel[] parcels = new StatsEventParcel[data.size()];
+ for (int i = 0; i < data.size(); i++) {
+ parcels[i] = new StatsEventParcel();
+ parcels[i].buffer = data.get(i).getBytes();
+ }
+ try {
+ resultReceiver.pullFinished(atomTag, success, parcels);
+ } catch (RemoteException e) {
+ Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId
+ + " due to TransactionTooLarge. Calling pullFinish with no data");
+ StatsEventParcel[] emptyData = new StatsEventParcel[0];
+ try {
+ resultReceiver.pullFinished(atomTag, /*success=*/false, emptyData);
+ } catch (RemoteException nestedException) {
+ Log.w(TAG, "StatsPullResultReceiver failed for tag " + mAtomId
+ + " with empty payload");
+ }
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ /**
+ * Metadata required for registering a StatsPullAtomCallback.
+ * All fields are optional, and defaults will be used for fields that are unspecified.
+ *
+ */
+ public static class PullAtomMetadata {
+ private final long mCoolDownMillis;
+ private final long mTimeoutMillis;
+ private final int[] mAdditiveFields;
+
+ // Private Constructor for builder
+ private PullAtomMetadata(long coolDownMillis, long timeoutMillis, int[] additiveFields) {
+ mCoolDownMillis = coolDownMillis;
+ mTimeoutMillis = timeoutMillis;
+ mAdditiveFields = additiveFields;
+ }
+
+ /**
+ * Builder for PullAtomMetadata.
+ */
+ public static class Builder {
+ private long mCoolDownMillis;
+ private long mTimeoutMillis;
+ private int[] mAdditiveFields;
+
+ /**
+ * Returns a new PullAtomMetadata.Builder object for constructing PullAtomMetadata for
+ * StatsManager#registerPullAtomCallback
+ */
+ public Builder() {
+ mCoolDownMillis = DEFAULT_COOL_DOWN_MILLIS;
+ mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
+ mAdditiveFields = null;
+ }
+
+ /**
+ * Set the cool down time of the pull in milliseconds. If two successive pulls are
+ * issued within the cool down, a cached version of the first pull will be used for the
+ * second pull. The minimum allowed cool down is 1 second.
+ */
+ @NonNull
+ public Builder setCoolDownMillis(long coolDownMillis) {
+ mCoolDownMillis = coolDownMillis;
+ return this;
+ }
+
+ /**
+ * Set the maximum time the pull can take in milliseconds. The maximum allowed timeout
+ * is 10 seconds.
+ */
+ @NonNull
+ public Builder setTimeoutMillis(long timeoutMillis) {
+ mTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Set the additive fields of this pulled atom.
+ *
+ * This is only applicable for atoms which have a uid field. When tasks are run in
+ * isolated processes, the data will be attributed to the host uid. Additive fields
+ * will be combined when the non-additive fields are the same.
+ */
+ @NonNull
+ public Builder setAdditiveFields(@NonNull int[] additiveFields) {
+ mAdditiveFields = additiveFields;
+ return this;
+ }
+
+ /**
+ * Builds and returns a PullAtomMetadata object with the values set in the builder and
+ * defaults for unset fields.
+ */
+ @NonNull
+ public PullAtomMetadata build() {
+ return new PullAtomMetadata(mCoolDownMillis, mTimeoutMillis, mAdditiveFields);
+ }
+ }
+
+ /**
+ * Return the cool down time of this pull in milliseconds.
+ */
+ public long getCoolDownMillis() {
+ return mCoolDownMillis;
+ }
+
+ /**
+ * Return the maximum amount of time this pull can take in milliseconds.
+ */
+ public long getTimeoutMillis() {
+ return mTimeoutMillis;
+ }
+
+ /**
+ * Return the additive fields of this pulled atom.
+ *
+ * This is only applicable for atoms that have a uid field. When tasks are run in
+ * isolated processes, the data will be attributed to the host uid. Additive fields
+ * will be combined when the non-additive fields are the same.
+ */
+ @Nullable
+ public int[] getAdditiveFields() {
+ return mAdditiveFields;
+ }
+ }
+
+ /**
+ * Callback interface for pulling atoms requested by the stats service.
+ *
+ */
+ public interface StatsPullAtomCallback {
+ /**
+ * Pull data for the specified atom tag, filling in the provided list of StatsEvent data.
+ * @return {@link #PULL_SUCCESS} if the pull was successful, or {@link #PULL_SKIP} if not.
+ */
+ int onPullAtom(int atomTag, @NonNull List<StatsEvent> data);
+ }
+
+ @GuardedBy("sLock")
+ private IStatsManagerService getIStatsManagerServiceLocked() {
+ if (mStatsManagerService != null) {
+ return mStatsManagerService;
+ }
+ mStatsManagerService = IStatsManagerService.Stub.asInterface(
+ StatsFrameworkInitializer
+ .getStatsServiceManager()
+ .getStatsManagerServiceRegisterer()
+ .get());
+ return mStatsManagerService;
+ }
+
+ private static class StatsQueryCallbackInternal extends IStatsQueryCallback.Stub {
+ OutcomeReceiver<StatsCursor, StatsQueryException> queryCallback;
+ Executor mExecutor;
+
+ StatsQueryCallbackInternal(OutcomeReceiver<StatsCursor, StatsQueryException> queryCallback,
+ @NonNull @CallbackExecutor Executor executor) {
+ this.queryCallback = queryCallback;
+ this.mExecutor = executor;
+ }
+
+ @Override
+ public void sendResults(String[] queryData, String[] columnNames, int[] columnTypes,
+ int rowCount) {
+ if (!SdkLevel.isAtLeastU()) {
+ throw new IllegalStateException(
+ "StatsManager#query is not available before Android U");
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ StatsCursor cursor = new StatsCursor(queryData, columnNames, columnTypes,
+ rowCount);
+ queryCallback.onResult(cursor);
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void sendFailure(String error) {
+ if (!SdkLevel.isAtLeastU()) {
+ throw new IllegalStateException(
+ "StatsManager#query is not available before Android U");
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ queryCallback.onError(new StatsQueryException(error));
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ /**
+ * Exception thrown when communication with the stats service fails (eg if it is not available).
+ * This might be thrown early during boot before the stats service has started or if it crashed.
+ */
+ public static class StatsUnavailableException extends AndroidException {
+ public StatsUnavailableException(String reason) {
+ super("Failed to connect to statsd: " + reason);
+ }
+
+ public StatsUnavailableException(String reason, Throwable e) {
+ super("Failed to connect to statsd: " + reason, e);
+ }
+ }
+
+ /**
+ * Exception thrown when executing a query in statsd fails for any reason. This might be thrown
+ * if the query is malformed or if there is a database error when executing the query.
+ */
+ public static class StatsQueryException extends AndroidException {
+ public StatsQueryException(@NonNull String reason) {
+ super("Failed to query statsd: " + reason);
+ }
+
+ public StatsQueryException(@NonNull String reason, @NonNull Throwable e) {
+ super("Failed to query statsd: " + reason, e);
+ }
+ }
+}
diff --git a/android-34/android/app/StatsQuery.java b/android-34/android/app/StatsQuery.java
new file mode 100644
index 0000000..a4a315c
--- /dev/null
+++ b/android-34/android/app/StatsQuery.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+
+/**
+ * Represents a query that contains information required for StatsManager to return relevant metric
+ * data.
+ *
+ * @hide
+ */
+@SystemApi
+public final class StatsQuery {
+ /**
+ * Default value for SQL dialect.
+ */
+ public static final int DIALECT_UNKNOWN = 0;
+
+ /**
+ * Query passed is of SQLite dialect.
+ */
+ public static final int DIALECT_SQLITE = 1;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = {"DIALECT_"}, value = {DIALECT_UNKNOWN, DIALECT_SQLITE})
+ @interface SqlDialect {
+ }
+
+ private final int sqlDialect;
+ private final String rawSql;
+ private final int minClientSqlVersion;
+ private final byte[] policyConfig;
+ private StatsQuery(int sqlDialect, @NonNull String rawSql, int minClientSqlVersion,
+ @Nullable byte[] policyConfig) {
+ this.sqlDialect = sqlDialect;
+ this.rawSql = rawSql;
+ this.minClientSqlVersion = minClientSqlVersion;
+ this.policyConfig = policyConfig;
+ }
+
+ /**
+ * Returns the SQL dialect of the query.
+ */
+ public @SqlDialect int getSqlDialect() {
+ return sqlDialect;
+ }
+
+ /**
+ * Returns the raw SQL of the query.
+ */
+ @NonNull
+ public String getRawSql() {
+ return rawSql;
+ }
+
+ /**
+ * Returns the minimum SQL client library version required to execute the query.
+ */
+ @IntRange(from = 0)
+ public int getMinSqlClientVersion() {
+ return minClientSqlVersion;
+ }
+
+ /**
+ * Returns the wire-encoded StatsPolicyConfig proto that contains information to verify the
+ * query against a policy defined on the underlying data. Returns null if no policy was set.
+ */
+ @Nullable
+ public byte[] getPolicyConfig() {
+ return policyConfig;
+ }
+
+ /**
+ * Builder for constructing a StatsQuery object.
+ * <p>Usage:</p>
+ * <code>
+ * StatsQuery statsQuery = new StatsQuery.Builder("SELECT * from table")
+ * .setSqlDialect(StatsQuery.DIALECT_SQLITE)
+ * .setMinClientSqlVersion(1)
+ * .build();
+ * </code>
+ */
+ public static final class Builder {
+ private int sqlDialect;
+ private String rawSql;
+ private int minSqlClientVersion;
+ private byte[] policyConfig;
+
+ /**
+ * Returns a new StatsQuery.Builder object for constructing StatsQuery for
+ * StatsManager#query
+ */
+ public Builder(@NonNull final String rawSql) {
+ if (rawSql == null) {
+ throw new IllegalArgumentException("rawSql must not be null");
+ }
+ this.rawSql = rawSql;
+ this.sqlDialect = DIALECT_SQLITE;
+ this.minSqlClientVersion = 1;
+ this.policyConfig = null;
+ }
+
+ /**
+ * Sets the SQL dialect of the query.
+ *
+ * @param sqlDialect The SQL dialect of the query.
+ */
+ @NonNull
+ public Builder setSqlDialect(@SqlDialect final int sqlDialect) {
+ this.sqlDialect = sqlDialect;
+ return this;
+ }
+
+ /**
+ * Sets the minimum SQL client library version required to execute the query.
+ *
+ * @param minSqlClientVersion The minimum SQL client version required to execute the query.
+ */
+ @NonNull
+ public Builder setMinSqlClientVersion(@IntRange(from = 0) final int minSqlClientVersion) {
+ if (minSqlClientVersion < 0) {
+ throw new IllegalArgumentException("minSqlClientVersion must be a "
+ + "positive integer");
+ }
+ this.minSqlClientVersion = minSqlClientVersion;
+ return this;
+ }
+
+ /**
+ * Sets the wire-encoded StatsPolicyConfig proto that contains information to verify the
+ * query against a policy defined on the underlying data.
+ *
+ * @param policyConfig The wire-encoded StatsPolicyConfig proto.
+ */
+ @NonNull
+ public Builder setPolicyConfig(@NonNull final byte[] policyConfig) {
+ this.policyConfig = policyConfig;
+ return this;
+ }
+
+ /**
+ * Builds a new instance of {@link StatsQuery}.
+ *
+ * @return A new instance of {@link StatsQuery}.
+ */
+ @NonNull
+ public StatsQuery build() {
+ return new StatsQuery(sqlDialect, rawSql, minSqlClientVersion, policyConfig);
+ }
+ }
+}
diff --git a/android-34/android/app/admin/SecurityLogTags.java b/android-34/android/app/admin/SecurityLogTags.java
new file mode 100644
index 0000000..55c8e2b
--- /dev/null
+++ b/android-34/android/app/admin/SecurityLogTags.java
@@ -0,0 +1,313 @@
+/* This file is auto-generated. DO NOT MODIFY.
+ * Source file: frameworks/base/core/java/android/app/admin/SecurityLogTags.logtags
+ */
+
+package android.app.admin;
+
+/**
+ * @hide
+ */
+public class SecurityLogTags {
+ private SecurityLogTags() { } // don't instantiate
+
+ /** 210001 security_adb_shell_interactive */
+ public static final int SECURITY_ADB_SHELL_INTERACTIVE = 210001;
+
+ /** 210002 security_adb_shell_command (command|3) */
+ public static final int SECURITY_ADB_SHELL_COMMAND = 210002;
+
+ /** 210003 security_adb_sync_recv (path|3) */
+ public static final int SECURITY_ADB_SYNC_RECV = 210003;
+
+ /** 210004 security_adb_sync_send (path|3) */
+ public static final int SECURITY_ADB_SYNC_SEND = 210004;
+
+ /** 210005 security_app_process_start (process|3),(start_time|2|3),(uid|1),(pid|1),(seinfo|3),(sha256|3) */
+ public static final int SECURITY_APP_PROCESS_START = 210005;
+
+ /** 210006 security_keyguard_dismissed */
+ public static final int SECURITY_KEYGUARD_DISMISSED = 210006;
+
+ /** 210007 security_keyguard_dismiss_auth_attempt (success|1),(method_strength|1) */
+ public static final int SECURITY_KEYGUARD_DISMISS_AUTH_ATTEMPT = 210007;
+
+ /** 210008 security_keyguard_secured */
+ public static final int SECURITY_KEYGUARD_SECURED = 210008;
+
+ /** 210009 security_os_startup (boot_state|3),(verity_mode|3) */
+ public static final int SECURITY_OS_STARTUP = 210009;
+
+ /** 210010 security_os_shutdown */
+ public static final int SECURITY_OS_SHUTDOWN = 210010;
+
+ /** 210011 security_logging_started */
+ public static final int SECURITY_LOGGING_STARTED = 210011;
+
+ /** 210012 security_logging_stopped */
+ public static final int SECURITY_LOGGING_STOPPED = 210012;
+
+ /** 210013 security_media_mounted (path|3),(label|3) */
+ public static final int SECURITY_MEDIA_MOUNTED = 210013;
+
+ /** 210014 security_media_unmounted (path|3),(label|3) */
+ public static final int SECURITY_MEDIA_UNMOUNTED = 210014;
+
+ /** 210015 security_log_buffer_size_critical */
+ public static final int SECURITY_LOG_BUFFER_SIZE_CRITICAL = 210015;
+
+ /** 210016 security_password_expiration_set (package|3),(admin_user|1),(target_user|1),(timeout|2|3) */
+ public static final int SECURITY_PASSWORD_EXPIRATION_SET = 210016;
+
+ /** 210017 security_password_complexity_set (package|3),(admin_user|1),(target_user|1),(length|1),(quality|1),(num_letters|1),(num_non_letters|1),(num_numeric|1),(num_uppercase|1),(num_lowercase|1),(num_symbols|1) */
+ public static final int SECURITY_PASSWORD_COMPLEXITY_SET = 210017;
+
+ /** 210018 security_password_history_length_set (package|3),(admin_user|1),(target_user|1),(length|1) */
+ public static final int SECURITY_PASSWORD_HISTORY_LENGTH_SET = 210018;
+
+ /** 210019 security_max_screen_lock_timeout_set (package|3),(admin_user|1),(target_user|1),(timeout|2|3) */
+ public static final int SECURITY_MAX_SCREEN_LOCK_TIMEOUT_SET = 210019;
+
+ /** 210020 security_max_password_attempts_set (package|3),(admin_user|1),(target_user|1),(num_failures|1) */
+ public static final int SECURITY_MAX_PASSWORD_ATTEMPTS_SET = 210020;
+
+ /** 210021 security_keyguard_disabled_features_set (package|3),(admin_user|1),(target_user|1),(features|1) */
+ public static final int SECURITY_KEYGUARD_DISABLED_FEATURES_SET = 210021;
+
+ /** 210022 security_remote_lock (package|3),(admin_user|1),(target_user|1) */
+ public static final int SECURITY_REMOTE_LOCK = 210022;
+
+ /** 210023 security_wipe_failed (package|3),(admin_user|1) */
+ public static final int SECURITY_WIPE_FAILED = 210023;
+
+ /** 210024 security_key_generated (success|1),(key_id|3),(uid|1) */
+ public static final int SECURITY_KEY_GENERATED = 210024;
+
+ /** 210025 security_key_imported (success|1),(key_id|3),(uid|1) */
+ public static final int SECURITY_KEY_IMPORTED = 210025;
+
+ /** 210026 security_key_destroyed (success|1),(key_id|3),(uid|1) */
+ public static final int SECURITY_KEY_DESTROYED = 210026;
+
+ /** 210027 security_user_restriction_added (package|3),(admin_user|1),(restriction|3) */
+ public static final int SECURITY_USER_RESTRICTION_ADDED = 210027;
+
+ /** 210028 security_user_restriction_removed (package|3),(admin_user|1),(restriction|3) */
+ public static final int SECURITY_USER_RESTRICTION_REMOVED = 210028;
+
+ /** 210029 security_cert_authority_installed (success|1),(subject|3),(target_user|1) */
+ public static final int SECURITY_CERT_AUTHORITY_INSTALLED = 210029;
+
+ /** 210030 security_cert_authority_removed (success|1),(subject|3),(target_user|1) */
+ public static final int SECURITY_CERT_AUTHORITY_REMOVED = 210030;
+
+ /** 210031 security_crypto_self_test_completed (success|1) */
+ public static final int SECURITY_CRYPTO_SELF_TEST_COMPLETED = 210031;
+
+ /** 210032 security_key_integrity_violation (key_id|3),(uid|1) */
+ public static final int SECURITY_KEY_INTEGRITY_VIOLATION = 210032;
+
+ /** 210033 security_cert_validation_failure (reason|3) */
+ public static final int SECURITY_CERT_VALIDATION_FAILURE = 210033;
+
+ /** 210034 security_camera_policy_set (package|3),(admin_user|1),(target_user|1),(disabled|1) */
+ public static final int SECURITY_CAMERA_POLICY_SET = 210034;
+
+ /** 210035 security_password_complexity_required (package|3),(admin_user|1),(target_user|1),(complexity|1) */
+ public static final int SECURITY_PASSWORD_COMPLEXITY_REQUIRED = 210035;
+
+ /** 210036 security_password_changed (password_complexity|1),(target_user|1) */
+ public static final int SECURITY_PASSWORD_CHANGED = 210036;
+
+ /** 210037 security_wifi_connection (bssid|3),(event_type|3),(reason|3) */
+ public static final int SECURITY_WIFI_CONNECTION = 210037;
+
+ /** 210038 security_wifi_disconnection (bssid|3),(reason|3) */
+ public static final int SECURITY_WIFI_DISCONNECTION = 210038;
+
+ /** 210039 security_bluetooth_connection (addr|3),(success|1),(reason|3) */
+ public static final int SECURITY_BLUETOOTH_CONNECTION = 210039;
+
+ /** 210040 security_bluetooth_disconnection (addr|3),(reason|3) */
+ public static final int SECURITY_BLUETOOTH_DISCONNECTION = 210040;
+
+ /** 210041 security_package_installed (package_name|3),(version_code|1),(user_id|1) */
+ public static final int SECURITY_PACKAGE_INSTALLED = 210041;
+
+ /** 210042 security_package_updated (package_name|3),(version_code|1),(user_id|1) */
+ public static final int SECURITY_PACKAGE_UPDATED = 210042;
+
+ /** 210043 security_package_uninstalled (package_name|3),(version_code|1),(user_id|1) */
+ public static final int SECURITY_PACKAGE_UNINSTALLED = 210043;
+
+ public static void writeSecurityAdbShellInteractive() {
+ android.util.EventLog.writeEvent(SECURITY_ADB_SHELL_INTERACTIVE);
+ }
+
+ public static void writeSecurityAdbShellCommand(String command) {
+ android.util.EventLog.writeEvent(SECURITY_ADB_SHELL_COMMAND, command);
+ }
+
+ public static void writeSecurityAdbSyncRecv(String path) {
+ android.util.EventLog.writeEvent(SECURITY_ADB_SYNC_RECV, path);
+ }
+
+ public static void writeSecurityAdbSyncSend(String path) {
+ android.util.EventLog.writeEvent(SECURITY_ADB_SYNC_SEND, path);
+ }
+
+ public static void writeSecurityAppProcessStart(String process, long startTime, int uid, int pid, String seinfo, String sha256) {
+ android.util.EventLog.writeEvent(SECURITY_APP_PROCESS_START, process, startTime, uid, pid, seinfo, sha256);
+ }
+
+ public static void writeSecurityKeyguardDismissed() {
+ android.util.EventLog.writeEvent(SECURITY_KEYGUARD_DISMISSED);
+ }
+
+ public static void writeSecurityKeyguardDismissAuthAttempt(int success, int methodStrength) {
+ android.util.EventLog.writeEvent(SECURITY_KEYGUARD_DISMISS_AUTH_ATTEMPT, success, methodStrength);
+ }
+
+ public static void writeSecurityKeyguardSecured() {
+ android.util.EventLog.writeEvent(SECURITY_KEYGUARD_SECURED);
+ }
+
+ public static void writeSecurityOsStartup(String bootState, String verityMode) {
+ android.util.EventLog.writeEvent(SECURITY_OS_STARTUP, bootState, verityMode);
+ }
+
+ public static void writeSecurityOsShutdown() {
+ android.util.EventLog.writeEvent(SECURITY_OS_SHUTDOWN);
+ }
+
+ public static void writeSecurityLoggingStarted() {
+ android.util.EventLog.writeEvent(SECURITY_LOGGING_STARTED);
+ }
+
+ public static void writeSecurityLoggingStopped() {
+ android.util.EventLog.writeEvent(SECURITY_LOGGING_STOPPED);
+ }
+
+ public static void writeSecurityMediaMounted(String path, String label) {
+ android.util.EventLog.writeEvent(SECURITY_MEDIA_MOUNTED, path, label);
+ }
+
+ public static void writeSecurityMediaUnmounted(String path, String label) {
+ android.util.EventLog.writeEvent(SECURITY_MEDIA_UNMOUNTED, path, label);
+ }
+
+ public static void writeSecurityLogBufferSizeCritical() {
+ android.util.EventLog.writeEvent(SECURITY_LOG_BUFFER_SIZE_CRITICAL);
+ }
+
+ public static void writeSecurityPasswordExpirationSet(String package_, int adminUser, int targetUser, long timeout) {
+ android.util.EventLog.writeEvent(SECURITY_PASSWORD_EXPIRATION_SET, package_, adminUser, targetUser, timeout);
+ }
+
+ public static void writeSecurityPasswordComplexitySet(String package_, int adminUser, int targetUser, int length, int quality, int numLetters, int numNonLetters, int numNumeric, int numUppercase, int numLowercase, int numSymbols) {
+ android.util.EventLog.writeEvent(SECURITY_PASSWORD_COMPLEXITY_SET, package_, adminUser, targetUser, length, quality, numLetters, numNonLetters, numNumeric, numUppercase, numLowercase, numSymbols);
+ }
+
+ public static void writeSecurityPasswordHistoryLengthSet(String package_, int adminUser, int targetUser, int length) {
+ android.util.EventLog.writeEvent(SECURITY_PASSWORD_HISTORY_LENGTH_SET, package_, adminUser, targetUser, length);
+ }
+
+ public static void writeSecurityMaxScreenLockTimeoutSet(String package_, int adminUser, int targetUser, long timeout) {
+ android.util.EventLog.writeEvent(SECURITY_MAX_SCREEN_LOCK_TIMEOUT_SET, package_, adminUser, targetUser, timeout);
+ }
+
+ public static void writeSecurityMaxPasswordAttemptsSet(String package_, int adminUser, int targetUser, int numFailures) {
+ android.util.EventLog.writeEvent(SECURITY_MAX_PASSWORD_ATTEMPTS_SET, package_, adminUser, targetUser, numFailures);
+ }
+
+ public static void writeSecurityKeyguardDisabledFeaturesSet(String package_, int adminUser, int targetUser, int features) {
+ android.util.EventLog.writeEvent(SECURITY_KEYGUARD_DISABLED_FEATURES_SET, package_, adminUser, targetUser, features);
+ }
+
+ public static void writeSecurityRemoteLock(String package_, int adminUser, int targetUser) {
+ android.util.EventLog.writeEvent(SECURITY_REMOTE_LOCK, package_, adminUser, targetUser);
+ }
+
+ public static void writeSecurityWipeFailed(String package_, int adminUser) {
+ android.util.EventLog.writeEvent(SECURITY_WIPE_FAILED, package_, adminUser);
+ }
+
+ public static void writeSecurityKeyGenerated(int success, String keyId, int uid) {
+ android.util.EventLog.writeEvent(SECURITY_KEY_GENERATED, success, keyId, uid);
+ }
+
+ public static void writeSecurityKeyImported(int success, String keyId, int uid) {
+ android.util.EventLog.writeEvent(SECURITY_KEY_IMPORTED, success, keyId, uid);
+ }
+
+ public static void writeSecurityKeyDestroyed(int success, String keyId, int uid) {
+ android.util.EventLog.writeEvent(SECURITY_KEY_DESTROYED, success, keyId, uid);
+ }
+
+ public static void writeSecurityUserRestrictionAdded(String package_, int adminUser, String restriction) {
+ android.util.EventLog.writeEvent(SECURITY_USER_RESTRICTION_ADDED, package_, adminUser, restriction);
+ }
+
+ public static void writeSecurityUserRestrictionRemoved(String package_, int adminUser, String restriction) {
+ android.util.EventLog.writeEvent(SECURITY_USER_RESTRICTION_REMOVED, package_, adminUser, restriction);
+ }
+
+ public static void writeSecurityCertAuthorityInstalled(int success, String subject, int targetUser) {
+ android.util.EventLog.writeEvent(SECURITY_CERT_AUTHORITY_INSTALLED, success, subject, targetUser);
+ }
+
+ public static void writeSecurityCertAuthorityRemoved(int success, String subject, int targetUser) {
+ android.util.EventLog.writeEvent(SECURITY_CERT_AUTHORITY_REMOVED, success, subject, targetUser);
+ }
+
+ public static void writeSecurityCryptoSelfTestCompleted(int success) {
+ android.util.EventLog.writeEvent(SECURITY_CRYPTO_SELF_TEST_COMPLETED, success);
+ }
+
+ public static void writeSecurityKeyIntegrityViolation(String keyId, int uid) {
+ android.util.EventLog.writeEvent(SECURITY_KEY_INTEGRITY_VIOLATION, keyId, uid);
+ }
+
+ public static void writeSecurityCertValidationFailure(String reason) {
+ android.util.EventLog.writeEvent(SECURITY_CERT_VALIDATION_FAILURE, reason);
+ }
+
+ public static void writeSecurityCameraPolicySet(String package_, int adminUser, int targetUser, int disabled) {
+ android.util.EventLog.writeEvent(SECURITY_CAMERA_POLICY_SET, package_, adminUser, targetUser, disabled);
+ }
+
+ public static void writeSecurityPasswordComplexityRequired(String package_, int adminUser, int targetUser, int complexity) {
+ android.util.EventLog.writeEvent(SECURITY_PASSWORD_COMPLEXITY_REQUIRED, package_, adminUser, targetUser, complexity);
+ }
+
+ public static void writeSecurityPasswordChanged(int passwordComplexity, int targetUser) {
+ android.util.EventLog.writeEvent(SECURITY_PASSWORD_CHANGED, passwordComplexity, targetUser);
+ }
+
+ public static void writeSecurityWifiConnection(String bssid, String eventType, String reason) {
+ android.util.EventLog.writeEvent(SECURITY_WIFI_CONNECTION, bssid, eventType, reason);
+ }
+
+ public static void writeSecurityWifiDisconnection(String bssid, String reason) {
+ android.util.EventLog.writeEvent(SECURITY_WIFI_DISCONNECTION, bssid, reason);
+ }
+
+ public static void writeSecurityBluetoothConnection(String addr, int success, String reason) {
+ android.util.EventLog.writeEvent(SECURITY_BLUETOOTH_CONNECTION, addr, success, reason);
+ }
+
+ public static void writeSecurityBluetoothDisconnection(String addr, String reason) {
+ android.util.EventLog.writeEvent(SECURITY_BLUETOOTH_DISCONNECTION, addr, reason);
+ }
+
+ public static void writeSecurityPackageInstalled(String packageName, int versionCode, int userId) {
+ android.util.EventLog.writeEvent(SECURITY_PACKAGE_INSTALLED, packageName, versionCode, userId);
+ }
+
+ public static void writeSecurityPackageUpdated(String packageName, int versionCode, int userId) {
+ android.util.EventLog.writeEvent(SECURITY_PACKAGE_UPDATED, packageName, versionCode, userId);
+ }
+
+ public static void writeSecurityPackageUninstalled(String packageName, int versionCode, int userId) {
+ android.util.EventLog.writeEvent(SECURITY_PACKAGE_UNINSTALLED, packageName, versionCode, userId);
+ }
+}
diff --git a/android-34/android/app/adservices/AdServicesManager.java b/android-34/android/app/adservices/AdServicesManager.java
new file mode 100644
index 0000000..13f6875
--- /dev/null
+++ b/android-34/android/app/adservices/AdServicesManager.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.adservices;
+
+import static android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_MANAGER;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.app.adservices.consent.ConsentParcel;
+import android.app.adservices.topics.TopicParcel;
+import android.app.sdksandbox.SdkSandboxManager;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * AdServices Manager to handle the internal communication between PPAPI process and AdServices
+ * System Service.
+ *
+ * @hide
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public final class AdServicesManager {
+ @GuardedBy("SINGLETON_LOCK")
+ private static AdServicesManager sSingleton;
+
+ private final IAdServicesManager mService;
+ private static final Object SINGLETON_LOCK = new Object();
+
+ @IntDef(value = {MEASUREMENT_DELETION})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeletionApiType {}
+
+ public static final int MEASUREMENT_DELETION = 0;
+
+ // TODO(b/267789077): Create bit for other APIs.
+
+ @VisibleForTesting
+ public AdServicesManager(@NonNull IAdServicesManager iAdServicesManager) {
+ Objects.requireNonNull(iAdServicesManager, "AdServicesManager is NULL!");
+ mService = iAdServicesManager;
+ }
+
+ /** Get the singleton of AdServicesManager. Only used on T+ */
+ @Nullable
+ public static AdServicesManager getInstance(@NonNull Context context) {
+ synchronized (SINGLETON_LOCK) {
+ if (sSingleton == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // TODO(b/262282035): Fix this work around in U+.
+ // Get the AdServicesManagerService's Binder from the SdkSandboxManager.
+ // This is a workaround for b/262282035.
+ IBinder iBinder =
+ context.getSystemService(SdkSandboxManager.class).getAdServicesManager();
+ sSingleton = new AdServicesManager(IAdServicesManager.Stub.asInterface(iBinder));
+ }
+ }
+ return sSingleton;
+ }
+
+ /** Return the User Consent */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public ConsentParcel getConsent(@ConsentParcel.ConsentApiType int consentApiType) {
+ try {
+ return mService.getConsent(consentApiType);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Set the User Consent */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void setConsent(@NonNull ConsentParcel consentParcel) {
+ Objects.requireNonNull(consentParcel);
+ try {
+ mService.setConsent(consentParcel);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Saves information to the storage that notification was displayed for the first time to the
+ * user.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordNotificationDisplayed() {
+ try {
+ mService.recordNotificationDisplayed();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns information whether Consent Notification was displayed or not.
+ *
+ * @return true if Consent Notification was displayed, otherwise false.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean wasNotificationDisplayed() {
+ try {
+ return mService.wasNotificationDisplayed();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Saves information to the storage that notification was displayed for the first time to the
+ * user.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordGaUxNotificationDisplayed() {
+ try {
+ mService.recordGaUxNotificationDisplayed();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns information whether user interacted with consent manually.
+ *
+ * @return
+ * <ul>
+ * <li>-1 when no manual interaction was recorded
+ * <li>0 when no data about interaction (similar to null)
+ * <li>1 when manual interaction was recorded
+ * </ul>
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public int getUserManualInteractionWithConsent() {
+ try {
+ return mService.getUserManualInteractionWithConsent();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Saves information to the storage that user interacted with consent manually. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordUserManualInteractionWithConsent(int interaction) {
+ try {
+ mService.recordUserManualInteractionWithConsent(interaction);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns information whether Consent GA UX Notification was displayed or not.
+ *
+ * @return true if Consent GA UX Notification was displayed, otherwise false.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean wasGaUxNotificationDisplayed() {
+ try {
+ return mService.wasGaUxNotificationDisplayed();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Record a blocked topic.
+ *
+ * @param blockedTopicParcels the blocked topic to record
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordBlockedTopic(@NonNull List<TopicParcel> blockedTopicParcels) {
+ try {
+ mService.recordBlockedTopic(blockedTopicParcels);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove a blocked topic.
+ *
+ * @param blockedTopicParcel the blocked topic to remove
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void removeBlockedTopic(@NonNull TopicParcel blockedTopicParcel) {
+ try {
+ mService.removeBlockedTopic(blockedTopicParcel);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get all blocked topics.
+ *
+ * @return a {@code List} of all blocked topics.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public List<TopicParcel> retrieveAllBlockedTopics() {
+ try {
+ return mService.retrieveAllBlockedTopics();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Clear all Blocked Topics */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void clearAllBlockedTopics() {
+ try {
+ mService.clearAllBlockedTopics();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Returns the list of apps with consent. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public List<String> getKnownAppsWithConsent(List<String> installedPackages) {
+ try {
+ return mService.getKnownAppsWithConsent(installedPackages);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Returns the list of apps with revoked consent. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public List<String> getAppsWithRevokedConsent(List<String> installedPackages) {
+ try {
+ return mService.getAppsWithRevokedConsent(installedPackages);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Set user consent for an app */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void setConsentForApp(String packageName, int packageUid, boolean isConsentRevoked) {
+ try {
+ mService.setConsentForApp(packageName, packageUid, isConsentRevoked);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Reset all apps and blocked apps. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void clearKnownAppsWithConsent() {
+ try {
+ mService.clearKnownAppsWithConsent();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Reset all apps consent. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void clearAllAppConsentData() {
+ try {
+ mService.clearAllAppConsentData();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get if user consent is revoked for a given app.
+ *
+ * @return {@code true} if the user consent was revoked.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean isConsentRevokedForApp(String packageName, int packageUid) {
+ try {
+ return mService.isConsentRevokedForApp(packageName, packageUid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set user consent if the app first time request access and/or return consent value for the
+ * app.
+ *
+ * @return {@code true} if user consent was given.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean setConsentForAppIfNew(
+ String packageName, int packageUid, boolean isConsentRevoked) {
+ try {
+ return mService.setConsentForAppIfNew(packageName, packageUid, isConsentRevoked);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Clear the app consent entry for uninstalled app. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void clearConsentForUninstalledApp(String packageName, int packageUid) {
+ try {
+ mService.clearConsentForUninstalledApp(packageName, packageUid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Saves information to the storage that a deletion of measurement data occurred. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordAdServicesDeletionOccurred(@DeletionApiType int deletionType) {
+ try {
+ mService.recordAdServicesDeletionOccurred(deletionType);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Saves the PP API default consent of a user. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordDefaultConsent(boolean defaultConsent) {
+ try {
+ mService.recordDefaultConsent(defaultConsent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Saves the topics default consent of a user. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordTopicsDefaultConsent(boolean defaultConsent) {
+ try {
+ mService.recordTopicsDefaultConsent(defaultConsent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Saves the FLEDGE default consent of a user. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordFledgeDefaultConsent(boolean defaultConsent) {
+ try {
+ mService.recordFledgeDefaultConsent(defaultConsent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Saves the measurement default consent of a user. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordMeasurementDefaultConsent(boolean defaultConsent) {
+ try {
+ mService.recordMeasurementDefaultConsent(defaultConsent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Saves the default AdId state of a user. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void recordDefaultAdIdState(boolean defaultAdIdState) {
+ try {
+ mService.recordDefaultAdIdState(defaultAdIdState);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Checks whether the AdServices module needs to handle data reconciliation after a rollback.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean needsToHandleRollbackReconciliation(@DeletionApiType int deletionType) {
+ try {
+ return mService.needsToHandleRollbackReconciliation(deletionType);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the PP API default consent of a user.
+ *
+ * @return true if the PP API default consent is given, false otherwise.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean getDefaultConsent() {
+ try {
+ return mService.getDefaultConsent();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the topics default consent of a user.
+ *
+ * @return true if the topics default consent is given, false otherwise.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean getTopicsDefaultConsent() {
+ try {
+ return mService.getTopicsDefaultConsent();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the FLEDGE default consent of a user.
+ *
+ * @return true if the FLEDGE default consent is given, false otherwise.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean getFledgeDefaultConsent() {
+ try {
+ return mService.getFledgeDefaultConsent();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the measurement default consent of a user.
+ *
+ * @return true if the measurement default consent is given, false otherwise.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean getMeasurementDefaultConsent() {
+ try {
+ return mService.getMeasurementDefaultConsent();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the default AdId state of a user.
+ *
+ * @return true if the default AdId State is enabled, false otherwise.
+ */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public boolean getDefaultAdIdState() {
+ try {
+ return mService.getDefaultAdIdState();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Returns the current privacy sandbox feature. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public String getCurrentPrivacySandboxFeature() {
+ try {
+ return mService.getCurrentPrivacySandboxFeature();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Set the current privacy sandbox feature. */
+ @RequiresPermission(ACCESS_ADSERVICES_MANAGER)
+ public void setCurrentPrivacySandboxFeature(String featureType) {
+ try {
+ mService.setCurrentPrivacySandboxFeature(featureType);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android-34/android/app/adservices/consent/ConsentParcel.java b/android-34/android/app/adservices/consent/ConsentParcel.java
new file mode 100644
index 0000000..81200ac
--- /dev/null
+++ b/android-34/android/app/adservices/consent/ConsentParcel.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.adservices.consent;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represent a User Consent.
+ *
+ * @hide
+ */
+public final class ConsentParcel implements Parcelable {
+ /**
+ * Consent Api Types.
+ *
+ * @hide
+ */
+ @IntDef(value = {UNKNOWN, ALL_API, TOPICS, FLEDGE, MEASUREMENT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConsentApiType {}
+
+ /** The Consent API Type is not set. */
+ public static final int UNKNOWN = 0;
+
+ /** The Consent API Type for All API. This is used when there is only 1 consent for all APIs */
+ public static final int ALL_API = 1;
+
+ /** The Consent API Type for Topics. */
+ public static final int TOPICS = 2;
+
+ /** The Consent API Type for FLEDGE. */
+ public static final int FLEDGE = 3;
+
+ /** The Consent API Type for Measurement. */
+ public static final int MEASUREMENT = 4;
+
+ private final boolean mIsGiven;
+ @ConsentApiType private final int mConsentApiType;
+
+ private ConsentParcel(@NonNull Builder builder) {
+ mIsGiven = builder.mIsGiven;
+ mConsentApiType = builder.mConsentApiType;
+ }
+
+ private ConsentParcel(@NonNull Parcel in) {
+ mConsentApiType = in.readInt();
+ mIsGiven = in.readBoolean();
+ }
+
+ public static final @NonNull Creator<ConsentParcel> CREATOR =
+ new Parcelable.Creator<ConsentParcel>() {
+ @Override
+ public ConsentParcel createFromParcel(Parcel in) {
+ return new ConsentParcel(in);
+ }
+
+ @Override
+ public ConsentParcel[] newArray(int size) {
+ return new ConsentParcel[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mConsentApiType);
+ out.writeBoolean(mIsGiven);
+ }
+
+ /** Get the ConsentApiType. */
+ @ConsentApiType
+ public int getConsentApiType() {
+ return mConsentApiType;
+ }
+
+ /** Get the IsGiven. */
+ public boolean isIsGiven() {
+ return mIsGiven;
+ }
+
+ /** Create a REVOKED consent for the consentApiType */
+ public static ConsentParcel createRevokedConsent(@ConsentApiType int consentApiType) {
+ return new ConsentParcel.Builder()
+ .setConsentApiType(consentApiType)
+ .setIsGiven(false)
+ .build();
+ }
+
+ /** Create a GIVEN consent for the consentApiType */
+ public static ConsentParcel createGivenConsent(@ConsentApiType int consentApiType) {
+ return new ConsentParcel.Builder()
+ .setConsentApiType(consentApiType)
+ .setIsGiven(true)
+ .build();
+ }
+
+ /** Builder for {@link ConsentParcel} objects. */
+ public static final class Builder {
+ @ConsentApiType private int mConsentApiType = UNKNOWN;
+ private boolean mIsGiven = false;
+
+ public Builder() {}
+
+ /** Set the ConsentApiType for this request */
+ public @NonNull Builder setConsentApiType(@ConsentApiType int consentApiType) {
+ mConsentApiType = consentApiType;
+ return this;
+ }
+
+ /** Set the IsGiven */
+ public @NonNull Builder setIsGiven(Boolean isGiven) {
+ // null input means isGiven = false
+ mIsGiven = isGiven != null ? isGiven : false;
+ return this;
+ }
+
+ /** Builds a {@link ConsentParcel} instance. */
+ public @NonNull ConsentParcel build() {
+
+ if (mConsentApiType == UNKNOWN) {
+ throw new IllegalArgumentException("One must set the valid ConsentApiType");
+ }
+
+ return new ConsentParcel(this);
+ }
+ }
+}
diff --git a/android-34/android/app/adservices/topics/TopicParcel.java b/android-34/android/app/adservices/topics/TopicParcel.java
new file mode 100644
index 0000000..eaab26b
--- /dev/null
+++ b/android-34/android/app/adservices/topics/TopicParcel.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.adservices.topics;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represents a Topic.
+ *
+ * @hide
+ */
+public final class TopicParcel implements Parcelable {
+ private final long mTaxonomyVersion;
+ private final long mModelVersion;
+ private final int mTopicId;
+
+ private TopicParcel(@NonNull Builder builder) {
+ mTaxonomyVersion = builder.mTaxonomyVersion;
+ mModelVersion = builder.mModelVersion;
+ mTopicId = builder.mTopicId;
+ }
+
+ private TopicParcel(@NonNull Parcel in) {
+ mTaxonomyVersion = in.readLong();
+ mModelVersion = in.readLong();
+ mTopicId = in.readInt();
+ }
+
+ @NonNull
+ public static final Creator<TopicParcel> CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ public TopicParcel createFromParcel(Parcel in) {
+ return new TopicParcel(in);
+ }
+
+ @Override
+ public TopicParcel[] newArray(int size) {
+ return new TopicParcel[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeLong(mTaxonomyVersion);
+ out.writeLong(mModelVersion);
+ out.writeInt(mTopicId);
+ }
+
+ /** Get the taxonomy version. */
+ public long getTaxonomyVersion() {
+ return mTaxonomyVersion;
+ }
+
+ /** Get the model Version. */
+ public long getModelVersion() {
+ return mModelVersion;
+ }
+
+ /** Get the Topic ID. */
+ public int getTopicId() {
+ return mTopicId;
+ }
+
+ /** Builder for {@link TopicParcel} objects. */
+ public static final class Builder {
+ private long mTaxonomyVersion;
+ private long mModelVersion;
+ private int mTopicId;
+
+ public Builder() {}
+
+ /** Set the taxonomy version */
+ @NonNull
+ public TopicParcel.Builder setTaxonomyVersion(long taxonomyVersion) {
+ mTaxonomyVersion = taxonomyVersion;
+ return this;
+ }
+
+ /** Set the model version */
+ @NonNull
+ public TopicParcel.Builder setModelVersion(long modelVersion) {
+ mModelVersion = modelVersion;
+ return this;
+ }
+
+ /** Set the topic id */
+ @NonNull
+ public TopicParcel.Builder setTopicId(int topicId) {
+ mTopicId = topicId;
+ return this;
+ }
+
+ /** Builds a {@link TopicParcel} instance. */
+ @NonNull
+ public TopicParcel build() {
+ return new TopicParcel(this);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getTaxonomyVersion(), getModelVersion(), getTopicId());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof TopicParcel)) {
+ return false;
+ }
+
+ TopicParcel topicParcel = (TopicParcel) obj;
+ return this.getTaxonomyVersion() == topicParcel.getTaxonomyVersion()
+ && this.getModelVersion() == topicParcel.getModelVersion()
+ && this.getTopicId() == topicParcel.getTopicId();
+ }
+}
diff --git a/android-34/android/app/appsearch/AppSearchBatchResult.java b/android-34/android/app/appsearch/AppSearchBatchResult.java
new file mode 100644
index 0000000..efd5e31
--- /dev/null
+++ b/android-34/android/app/appsearch/AppSearchBatchResult.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.util.ArrayMap;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Provides results for AppSearch batch operations which encompass multiple documents.
+ *
+ * <p>Individual results of a batch operation are separated into two maps: one for successes and one
+ * for failures. For successes, {@link #getSuccesses()} will return a map of keys to instances of
+ * the value type. For failures, {@link #getFailures()} will return a map of keys to {@link
+ * AppSearchResult} objects.
+ *
+ * <p>Alternatively, {@link #getAll()} returns a map of keys to {@link AppSearchResult} objects for
+ * both successes and failures.
+ *
+ * @param <KeyType> The type of the keys for which the results will be reported.
+ * @param <ValueType> The type of the result objects for successful results.
+ * @see AppSearchSession#put
+ * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#remove
+ */
+public final class AppSearchBatchResult<KeyType, ValueType> {
+ @NonNull private final Map<KeyType, ValueType> mSuccesses;
+ @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mFailures;
+ @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mAll;
+
+ AppSearchBatchResult(
+ @NonNull Map<KeyType, ValueType> successes,
+ @NonNull Map<KeyType, AppSearchResult<ValueType>> failures,
+ @NonNull Map<KeyType, AppSearchResult<ValueType>> all) {
+ mSuccesses = Objects.requireNonNull(successes);
+ mFailures = Objects.requireNonNull(failures);
+ mAll = Objects.requireNonNull(all);
+ }
+
+ /** Returns {@code true} if this {@link AppSearchBatchResult} has no failures. */
+ public boolean isSuccess() {
+ return mFailures.isEmpty();
+ }
+
+ /**
+ * Returns a {@link Map} of keys mapped to instances of the value type for all successful
+ * individual results.
+ *
+ * <p>Example: {@link AppSearchSession#getByDocumentId} returns an {@link AppSearchBatchResult}.
+ * Each key (the document ID, of {@code String} type) will map to a {@link GenericDocument}
+ * object.
+ *
+ * <p>The values of the {@link Map} will not be {@code null}.
+ */
+ @NonNull
+ public Map<KeyType, ValueType> getSuccesses() {
+ return Collections.unmodifiableMap(mSuccesses);
+ }
+
+ /**
+ * Returns a {@link Map} of keys mapped to instances of {@link AppSearchResult} for all failed
+ * individual results.
+ *
+ * <p>The values of the {@link Map} will not be {@code null}.
+ */
+ @NonNull
+ public Map<KeyType, AppSearchResult<ValueType>> getFailures() {
+ return Collections.unmodifiableMap(mFailures);
+ }
+
+ /**
+ * Returns a {@link Map} of keys mapped to instances of {@link AppSearchResult} for all
+ * individual results.
+ *
+ * <p>The values of the {@link Map} will not be {@code null}.
+ */
+ @NonNull
+ public Map<KeyType, AppSearchResult<ValueType>> getAll() {
+ return Collections.unmodifiableMap(mAll);
+ }
+
+ /**
+ * Asserts that this {@link AppSearchBatchResult} has no failures.
+ *
+ * @hide
+ */
+ public void checkSuccess() {
+ if (!isSuccess()) {
+ throw new IllegalStateException("AppSearchBatchResult has failures: " + this);
+ }
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "{\n successes: " + mSuccesses + "\n failures: " + mFailures + "\n}";
+ }
+
+ /**
+ * Builder for {@link AppSearchBatchResult} objects.
+ *
+ * @param <KeyType> The type of the keys for which the results will be reported.
+ * @param <ValueType> The type of the result objects for successful results.
+ */
+ public static final class Builder<KeyType, ValueType> {
+ private ArrayMap<KeyType, ValueType> mSuccesses = new ArrayMap<>();
+ private ArrayMap<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();
+ private ArrayMap<KeyType, AppSearchResult<ValueType>> mAll = new ArrayMap<>();
+ private boolean mBuilt = false;
+
+ /**
+ * Associates the {@code key} with the provided successful return value.
+ *
+ * <p>Any previous mapping for a key, whether success or failure, is deleted.
+ *
+ * <p>This is a convenience function which is equivalent to {@code setResult(key,
+ * AppSearchResult.newSuccessfulResult(value))}.
+ *
+ * @param key The key to associate the result with; usually corresponds to some identifier
+ * from the input like an ID or name.
+ * @param value An optional value to associate with the successful result of the operation
+ * being performed.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // See getSuccesses
+ @NonNull
+ public Builder<KeyType, ValueType> setSuccess(
+ @NonNull KeyType key, @Nullable ValueType value) {
+ Objects.requireNonNull(key);
+ resetIfBuilt();
+ return setResult(key, AppSearchResult.newSuccessfulResult(value));
+ }
+
+ /**
+ * Associates the {@code key} with the provided failure code and error message.
+ *
+ * <p>Any previous mapping for a key, whether success or failure, is deleted.
+ *
+ * <p>This is a convenience function which is equivalent to {@code setResult(key,
+ * AppSearchResult.newFailedResult(resultCode, errorMessage))}.
+ *
+ * @param key The key to associate the result with; usually corresponds to some identifier
+ * from the input like an ID or name.
+ * @param resultCode One of the constants documented in {@link
+ * AppSearchResult#getResultCode}.
+ * @param errorMessage An optional string describing the reason or nature of the failure.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // See getFailures
+ @NonNull
+ public Builder<KeyType, ValueType> setFailure(
+ @NonNull KeyType key,
+ @AppSearchResult.ResultCode int resultCode,
+ @Nullable String errorMessage) {
+ Objects.requireNonNull(key);
+ resetIfBuilt();
+ return setResult(key, AppSearchResult.newFailedResult(resultCode, errorMessage));
+ }
+
+ /**
+ * Associates the {@code key} with the provided {@code result}.
+ *
+ * <p>Any previous mapping for a key, whether success or failure, is deleted.
+ *
+ * @param key The key to associate the result with; usually corresponds to some identifier
+ * from the input like an ID or name.
+ * @param result The result to associate with the key.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // See getAll
+ @NonNull
+ public Builder<KeyType, ValueType> setResult(
+ @NonNull KeyType key, @NonNull AppSearchResult<ValueType> result) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(result);
+ resetIfBuilt();
+ if (result.isSuccess()) {
+ mSuccesses.put(key, result.getResultValue());
+ mFailures.remove(key);
+ } else {
+ mFailures.put(key, result);
+ mSuccesses.remove(key);
+ }
+ mAll.put(key, result);
+ return this;
+ }
+
+ /**
+ * Builds an {@link AppSearchBatchResult} object from the contents of this {@link Builder}.
+ */
+ @NonNull
+ public AppSearchBatchResult<KeyType, ValueType> build() {
+ mBuilt = true;
+ return new AppSearchBatchResult<>(mSuccesses, mFailures, mAll);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mSuccesses = new ArrayMap<>(mSuccesses);
+ mFailures = new ArrayMap<>(mFailures);
+ mAll = new ArrayMap<>(mAll);
+ mBuilt = false;
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/AppSearchManager.java b/android-34/android/app/appsearch/AppSearchManager.java
new file mode 100644
index 0000000..0a0346f
--- /dev/null
+++ b/android-34/android/app/appsearch/AppSearchManager.java
@@ -0,0 +1,239 @@
+/*
+ * 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 android.app.appsearch;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.annotation.UserHandleAware;
+import android.app.appsearch.aidl.IAppSearchManager;
+import android.content.Context;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Provides access to the centralized AppSearch index maintained by the system.
+ *
+ * <p>AppSearch is an offline, on-device search library for managing structured data featuring:
+ *
+ * <ul>
+ * <li>APIs to index and retrieve data via full-text search.
+ * <li>An API for applications to explicitly grant read-access permission of their data to other
+ * applications.
+ * <b>See: {@link SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}</b>
+ * <li>An API for applications to opt into or out of having their data displayed on System UI
+ * surfaces by the System-designated global querier.
+ * <b>See: {@link SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}</b>
+ * </ul>
+ *
+ * <p>Applications create a database by opening an {@link AppSearchSession}.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class);
+ *
+ * AppSearchManager.SearchContext searchContext = new AppSearchManager.SearchContext.Builder().
+ * setDatabaseName(dbName).build());
+ * appSearchManager.createSearchSession(searchContext, mExecutor, appSearchSessionResult -> {
+ * mAppSearchSession = appSearchSessionResult.getResultValue();
+ * });</pre>
+ *
+ * <p>After opening the session, a schema must be set in order to define the organizational
+ * structure of data. The schema is set by calling {@link AppSearchSession#setSchema}. The schema is
+ * composed of a collection of {@link AppSearchSchema} objects, each of which defines a unique type
+ * of data.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * AppSearchSchema emailSchemaType = new AppSearchSchema.Builder("Email")
+ * .addProperty(new StringPropertyConfig.Builder("subject")
+ * .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ * .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+ * .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+ * .build()
+ * ).build();
+ *
+ * SetSchemaRequest request = new SetSchemaRequest.Builder().addSchema(emailSchemaType).build();
+ * mAppSearchSession.set(request, mExecutor, appSearchResult -> {
+ * if (appSearchResult.isSuccess()) {
+ * //Schema has been successfully set.
+ * }
+ * });</pre>
+ *
+ * <p>The basic unit of data in AppSearch is represented as a {@link GenericDocument} object,
+ * containing an ID, namespace, time-to-live, score, and properties. A namespace organizes a logical
+ * group of documents. For example, a namespace can be created to group documents on a per-account
+ * basis. An ID identifies a single document within a namespace. The combination of namespace and ID
+ * uniquely identifies a {@link GenericDocument} in the database.
+ *
+ * <p>Once the schema has been set, {@link GenericDocument} objects can be put into the database and
+ * indexed by calling {@link AppSearchSession#put}.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * // Although for this example we use GenericDocument directly, we recommend extending
+ * // GenericDocument to create specific types (i.e. Email) with specific setters/getters.
+ * GenericDocument email = new GenericDocument.Builder<>(NAMESPACE, ID, EMAIL_SCHEMA_TYPE)
+ * .setPropertyString(“subject”, EMAIL_SUBJECT)
+ * .setScore(EMAIL_SCORE)
+ * .build();
+ *
+ * PutDocumentsRequest request = new PutDocumentsRequest.Builder().addGenericDocuments(email)
+ * .build();
+ * mAppSearchSession.put(request, mExecutor, appSearchBatchResult -> {
+ * if (appSearchBatchResult.isSuccess()) {
+ * //All documents have been successfully indexed.
+ * }
+ * });</pre>
+ *
+ * <p>Searching within the database is done by calling {@link AppSearchSession#search} and providing
+ * the query string to search for, as well as a {@link SearchSpec}.
+ *
+ * <p>Alternatively, {@link AppSearchSession#getByDocumentId} can be called to retrieve documents by
+ * namespace and ID.
+ *
+ * <p>Document removal is done either by time-to-live expiration, or explicitly calling a remove
+ * operation. Remove operations can be done by namespace and ID via {@link
+ * AppSearchSession#remove(RemoveByDocumentIdRequest, Executor, BatchResultCallback)}, or by query
+ * via {@link AppSearchSession#remove(String, SearchSpec, Executor, Consumer)}.
+ */
+@SystemService(Context.APP_SEARCH_SERVICE)
+public class AppSearchManager {
+
+ private final IAppSearchManager mService;
+ private final Context mContext;
+
+ /** @hide */
+ public AppSearchManager(@NonNull Context context, @NonNull IAppSearchManager service) {
+ mContext = Objects.requireNonNull(context);
+ mService = Objects.requireNonNull(service);
+ }
+
+ /** Contains information about how to create the search session. */
+ public static final class SearchContext {
+ final String mDatabaseName;
+
+ SearchContext(@NonNull String databaseName) {
+ mDatabaseName = Objects.requireNonNull(databaseName);
+ }
+
+ /**
+ * Returns the name of the database to create or open.
+ *
+ * <p>Databases with different names are fully separate with distinct types, namespaces, and
+ * data.
+ */
+ @NonNull
+ public String getDatabaseName() {
+ return mDatabaseName;
+ }
+
+ /** Builder for {@link SearchContext} objects. */
+ public static final class Builder {
+ private final String mDatabaseName;
+ private boolean mBuilt = false;
+
+ /**
+ * Creates a new {@link SearchContext.Builder}.
+ *
+ * <p>{@link AppSearchSession} will create or open a database under the given name.
+ *
+ * <p>Databases with different names are fully separate with distinct types, namespaces,
+ * and data.
+ *
+ * <p>Database name cannot contain {@code '/'}.
+ *
+ * @param databaseName The name of the database.
+ * @throws IllegalArgumentException if the databaseName contains {@code '/'}.
+ */
+ public Builder(@NonNull String databaseName) {
+ Objects.requireNonNull(databaseName);
+ Preconditions.checkArgument(
+ !databaseName.contains("/"), "Database name cannot contain '/'");
+ mDatabaseName = databaseName;
+ }
+
+ /** Builds a {@link SearchContext} instance. */
+ @NonNull
+ public SearchContext build() {
+ Preconditions.checkState(!mBuilt, "Builder has already been used");
+ mBuilt = true;
+ return new SearchContext(mDatabaseName);
+ }
+ }
+ }
+
+ /**
+ * Creates a new {@link AppSearchSession}.
+ *
+ * <p>This process requires an AppSearch native indexing file system. If it's not created, the
+ * initialization process will create one under the user's credential encrypted directory.
+ *
+ * @param searchContext The {@link SearchContext} contains all information to create a new
+ * {@link AppSearchSession}
+ * @param executor Executor on which to invoke the callback.
+ * @param callback The {@link AppSearchResult}<{@link AppSearchSession}> of performing
+ * this operation. Or a {@link AppSearchResult} with failure reason code and error
+ * information.
+ */
+ @UserHandleAware
+ public void createSearchSession(
+ @NonNull SearchContext searchContext,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
+ Objects.requireNonNull(searchContext);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ AppSearchSession.createSearchSession(
+ searchContext,
+ mService,
+ mContext.getUser(),
+ mContext.getAttributionSource(),
+ executor,
+ callback);
+ }
+
+ /**
+ * Creates a new {@link GlobalSearchSession}.
+ *
+ * <p>This process requires an AppSearch native indexing file system. If it's not created, the
+ * initialization process will create one under the user's credential encrypted directory.
+ *
+ * @param executor Executor on which to invoke the callback.
+ * @param callback The {@link AppSearchResult}<{@link GlobalSearchSession}> of performing
+ * this operation. Or a {@link AppSearchResult} with failure reason code and error
+ * information.
+ */
+ @UserHandleAware
+ public void createGlobalSearchSession(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ GlobalSearchSession.createGlobalSearchSession(
+ mService,
+ mContext.getUser(),
+ mContext.getAttributionSource(),
+ executor, callback);
+ }
+}
diff --git a/android-34/android/app/appsearch/AppSearchManagerFrameworkInitializer.java b/android-34/android/app/appsearch/AppSearchManagerFrameworkInitializer.java
new file mode 100644
index 0000000..7dc527a
--- /dev/null
+++ b/android-34/android/app/appsearch/AppSearchManagerFrameworkInitializer.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.appsearch;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.app.appsearch.aidl.IAppSearchManager;
+import android.content.Context;
+
+/**
+ * Class holding initialization code for the AppSearch module.
+ *
+ * @hide
+ */
+@SystemApi
+public class AppSearchManagerFrameworkInitializer {
+ private AppSearchManagerFrameworkInitializer() {}
+
+ /**
+ * Called by {@link SystemServiceRegistry}'s static initializer and registers all AppSearch
+ * services to {@link Context}, so that {@link Context#getSystemService} can return them.
+ *
+ * @throws IllegalStateException if this is called from anywhere besides
+ * {@link SystemServiceRegistry}
+ */
+ public static void initialize() {
+ SystemServiceRegistry.registerContextAwareService(
+ Context.APP_SEARCH_SERVICE, AppSearchManager.class,
+ (context, service) ->
+ new AppSearchManager(context, IAppSearchManager.Stub.asInterface(service)));
+ }
+}
diff --git a/android-34/android/app/appsearch/AppSearchMigrationHelper.java b/android-34/android/app/appsearch/AppSearchMigrationHelper.java
new file mode 100644
index 0000000..9e0e6db
--- /dev/null
+++ b/android-34/android/app/appsearch/AppSearchMigrationHelper.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import static android.app.appsearch.AppSearchResult.RESULT_INVALID_SCHEMA;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.WorkerThread;
+import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.IAppSearchManager;
+import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.stats.SchemaMigrationStats;
+import android.content.AttributionSource;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.ArraySet;
+
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * The helper class for {@link AppSearchSchema} migration.
+ *
+ * <p>It will query and migrate {@link GenericDocument} in given type to a new version.
+ * @hide
+ */
+public class AppSearchMigrationHelper implements Closeable {
+ private final IAppSearchManager mService;
+ private final AttributionSource mCallerAttributionSource;
+ private final String mDatabaseName;
+ private final UserHandle mUserHandle;
+ private final File mMigratedFile;
+ private final Set<String> mDestinationTypes;
+ private int mTotalNeedMigratedDocumentCount = 0;
+
+ AppSearchMigrationHelper(@NonNull IAppSearchManager service,
+ @NonNull UserHandle userHandle,
+ @NonNull AttributionSource callerAttributionSource,
+ @NonNull String databaseName,
+ @NonNull Set<AppSearchSchema> newSchemas) throws IOException {
+ mService = Objects.requireNonNull(service);
+ mUserHandle = Objects.requireNonNull(userHandle);
+ mCallerAttributionSource = Objects.requireNonNull(callerAttributionSource);
+ mDatabaseName = Objects.requireNonNull(databaseName);
+ mMigratedFile = File.createTempFile(/*prefix=*/"appsearch", /*suffix=*/null);
+ mDestinationTypes = new ArraySet<>(newSchemas.size());
+ for (AppSearchSchema newSchema : newSchemas) {
+ mDestinationTypes.add(newSchema.getSchemaType());
+ }
+ }
+
+ /**
+ * Queries all documents that need to be migrated to a different version and transform
+ * documents to that version by passing them to the provided {@link Migrator}.
+ *
+ * <p>The method will be executed on the executor provided to
+ * {@link AppSearchSession#setSchema}.
+ *
+ * @param schemaType The schema type that needs to be updated and whose {@link GenericDocument}
+ * need to be migrated.
+ * @param migrator The {@link Migrator} that will upgrade or downgrade a {@link
+ * GenericDocument} to new version.
+ * @param schemaMigrationStatsBuilder The {@link SchemaMigrationStats.Builder} contains
+ * schema migration stats information
+ */
+ @WorkerThread
+ void queryAndTransform(@NonNull String schemaType, @NonNull Migrator migrator,
+ int currentVersion, int finalVersion,
+ @Nullable SchemaMigrationStats.Builder schemaMigrationStatsBuilder)
+ throws IOException, AppSearchException, InterruptedException, ExecutionException {
+ File queryFile = File.createTempFile(/*prefix=*/"appsearch", /*suffix=*/null);
+ try (ParcelFileDescriptor fileDescriptor =
+ ParcelFileDescriptor.open(queryFile, MODE_WRITE_ONLY)) {
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicReference<AppSearchResult<Void>> resultReference = new AtomicReference<>();
+ mService.writeQueryResultsToFile(mCallerAttributionSource, mDatabaseName,
+ fileDescriptor,
+ /*queryExpression=*/ "",
+ new SearchSpec.Builder()
+ .addFilterSchemas(schemaType)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build().getBundle(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ resultReference.set(resultParcel.getResult());
+ latch.countDown();
+ }
+ });
+ latch.await();
+ AppSearchResult<Void> result = resultReference.get();
+ if (!result.isSuccess()) {
+ throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
+ }
+ readAndTransform(queryFile, migrator, currentVersion, finalVersion,
+ schemaMigrationStatsBuilder);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ queryFile.delete();
+ }
+ }
+
+ /**
+ * Puts all {@link GenericDocument} migrated from the previous call to
+ * {@link #queryAndTransform} into AppSearch.
+ *
+ * <p> This method should be only called once.
+ *
+ * @param responseBuilder a SetSchemaResponse builder whose result will be returned by this
+ * function with any
+ * {@link android.app.appsearch.SetSchemaResponse.MigrationFailure}
+ * added in.
+ * @param schemaMigrationStatsBuilder The {@link SchemaMigrationStats.Builder} contains
+ * schema migration stats information
+ * @param totalLatencyStartTimeMillis start timestamp to calculate total migration latency in
+ * Millis
+ * @return the {@link SetSchemaResponse} for {@link AppSearchSession#setSchema} call.
+ */
+ @NonNull
+ AppSearchResult<SetSchemaResponse> putMigratedDocuments(
+ @NonNull SetSchemaResponse.Builder responseBuilder,
+ @NonNull SchemaMigrationStats.Builder schemaMigrationStatsBuilder,
+ long totalLatencyStartTimeMillis) {
+ if (mTotalNeedMigratedDocumentCount == 0) {
+ return AppSearchResult.newSuccessfulResult(responseBuilder.build());
+ }
+ try (ParcelFileDescriptor fileDescriptor =
+ ParcelFileDescriptor.open(mMigratedFile, MODE_READ_ONLY)) {
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicReference<AppSearchResult<List<Bundle>>> resultReference =
+ new AtomicReference<>();
+ mService.putDocumentsFromFile(mCallerAttributionSource, mDatabaseName, fileDescriptor,
+ mUserHandle,
+ schemaMigrationStatsBuilder.build().getBundle(),
+ totalLatencyStartTimeMillis,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ resultReference.set(resultParcel.getResult());
+ latch.countDown();
+ }
+ });
+ latch.await();
+ AppSearchResult<List<Bundle>> result = resultReference.get();
+ if (!result.isSuccess()) {
+ return AppSearchResult.newFailedResult(result);
+ }
+ List<Bundle> migratedFailureBundles = Objects.requireNonNull(result.getResultValue());
+ for (int i = 0; i < migratedFailureBundles.size(); i++) {
+ responseBuilder.addMigrationFailure(
+ new SetSchemaResponse.MigrationFailure(migratedFailureBundles.get(i)));
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (Throwable t) {
+ return AppSearchResult.throwableToFailedResult(t);
+ } finally {
+ mMigratedFile.delete();
+ }
+ return AppSearchResult.newSuccessfulResult(responseBuilder.build());
+ }
+
+ /**
+ * Reads all saved {@link GenericDocument}s from the given {@link File}.
+ *
+ * <p>Transforms those {@link GenericDocument}s to the final version.
+ *
+ * <p>Save migrated {@link GenericDocument}s to the {@link #mMigratedFile}.
+ */
+ private void readAndTransform(@NonNull File file, @NonNull Migrator migrator,
+ int currentVersion, int finalVersion,
+ @Nullable SchemaMigrationStats.Builder schemaMigrationStatsBuilder)
+ throws IOException, AppSearchException {
+ try (DataInputStream inputStream = new DataInputStream(new FileInputStream(file));
+ DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(
+ mMigratedFile, /*append=*/ true))) {
+ GenericDocument document;
+ while (true) {
+ try {
+ document = readDocumentFromInputStream(inputStream);
+ } catch (EOFException e) {
+ break;
+ // Nothing wrong. We just finished reading.
+ }
+
+ GenericDocument newDocument;
+ if (currentVersion < finalVersion) {
+ newDocument = migrator.onUpgrade(currentVersion, finalVersion, document);
+ } else {
+ // currentVersion == finalVersion case won't trigger migration and get here.
+ newDocument = migrator.onDowngrade(currentVersion, finalVersion, document);
+ }
+ ++mTotalNeedMigratedDocumentCount;
+
+ if (!mDestinationTypes.contains(newDocument.getSchemaType())) {
+ // we exit before the new schema has been set to AppSearch. So no
+ // observable changes will be applied to stored schemas and documents.
+ // And the temp file will be deleted at close(), which will be triggered at
+ // the end of try-with-resources block of SearchSessionImpl.
+ throw new AppSearchException(
+ RESULT_INVALID_SCHEMA,
+ "Receive a migrated document with schema type: "
+ + newDocument.getSchemaType()
+ + ". But the schema types doesn't exist in the request");
+ }
+ writeBundleToOutputStream(outputStream, newDocument.getBundle());
+ }
+ }
+ if (schemaMigrationStatsBuilder != null) {
+ schemaMigrationStatsBuilder.setTotalNeedMigratedDocumentCount(
+ mTotalNeedMigratedDocumentCount);
+ }
+ }
+
+ /**
+ * Reads the {@link Bundle} of a {@link GenericDocument} from given {@link DataInputStream}.
+ *
+ * @param inputStream The inputStream to read from
+ *
+ * @throws IOException on read failure.
+ * @throws EOFException if {@link java.io.InputStream} reaches the end.
+ */
+ @NonNull
+ public static GenericDocument readDocumentFromInputStream(
+ @NonNull DataInputStream inputStream) throws IOException {
+ int length = inputStream.readInt();
+ if (length == 0) {
+ throw new EOFException();
+ }
+ byte[] serializedMessage = new byte[length];
+ inputStream.read(serializedMessage);
+
+ Parcel parcel = Parcel.obtain();
+ try {
+ parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
+ parcel.setDataPosition(0);
+ Bundle bundle = parcel.readBundle();
+ return new GenericDocument(bundle);
+ } finally {
+ parcel.recycle();
+ }
+ }
+
+ /**
+ * Serializes a {@link Bundle} and writes into the given {@link DataOutputStream}.
+ */
+ public static void writeBundleToOutputStream(
+ @NonNull DataOutputStream outputStream, @NonNull Bundle bundle)
+ throws IOException {
+ Parcel parcel = Parcel.obtain();
+ try {
+ parcel.writeBundle(bundle);
+ byte[] serializedMessage = parcel.marshall();
+ outputStream.writeInt(serializedMessage.length);
+ outputStream.write(serializedMessage);
+ } finally {
+ parcel.recycle();
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ mMigratedFile.delete();
+ }
+}
diff --git a/android-34/android/app/appsearch/AppSearchResult.java b/android-34/android/app/appsearch/AppSearchResult.java
new file mode 100644
index 0000000..5951db6
--- /dev/null
+++ b/android-34/android/app/appsearch/AppSearchResult.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.util.LogUtil;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Information about the success or failure of an AppSearch call.
+ *
+ * @param <ValueType> The type of result object for successful calls.
+ */
+public final class AppSearchResult<ValueType> {
+ private static final String TAG = "AppSearchResult";
+
+ /**
+ * Result codes from {@link AppSearchSession} methods.
+ *
+ * @hide
+ */
+ @IntDef(
+ value = {
+ RESULT_OK,
+ RESULT_UNKNOWN_ERROR,
+ RESULT_INTERNAL_ERROR,
+ RESULT_INVALID_ARGUMENT,
+ RESULT_IO_ERROR,
+ RESULT_OUT_OF_SPACE,
+ RESULT_NOT_FOUND,
+ RESULT_INVALID_SCHEMA,
+ RESULT_SECURITY_ERROR,
+ RESULT_DENIED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ResultCode {}
+
+ /** The call was successful. */
+ public static final int RESULT_OK = 0;
+
+ /** An unknown error occurred while processing the call. */
+ public static final int RESULT_UNKNOWN_ERROR = 1;
+
+ /**
+ * An internal error occurred within AppSearch, which the caller cannot address.
+ *
+ * <p>This error may be considered similar to {@link IllegalStateException}
+ */
+ public static final int RESULT_INTERNAL_ERROR = 2;
+
+ /**
+ * The caller supplied invalid arguments to the call.
+ *
+ * <p>This error may be considered similar to {@link IllegalArgumentException}.
+ */
+ public static final int RESULT_INVALID_ARGUMENT = 3;
+
+ /**
+ * An issue occurred reading or writing to storage. The call might succeed if repeated.
+ *
+ * <p>This error may be considered similar to {@link java.io.IOException}.
+ */
+ public static final int RESULT_IO_ERROR = 4;
+
+ /** Storage is out of space, and no more space could be reclaimed. */
+ public static final int RESULT_OUT_OF_SPACE = 5;
+
+ /** An entity the caller requested to interact with does not exist in the system. */
+ public static final int RESULT_NOT_FOUND = 6;
+
+ /** The caller supplied a schema which is invalid or incompatible with the previous schema. */
+ public static final int RESULT_INVALID_SCHEMA = 7;
+
+ /** The caller requested an operation it does not have privileges for. */
+ public static final int RESULT_SECURITY_ERROR = 8;
+
+ /**
+ * The requested operation is denied for the caller. This error is logged and returned for
+ * denylist rejections.
+ *
+ * @hide
+ */
+ // TODO(b/279047435): unhide this the next time we can make API changes
+ public static final int RESULT_DENIED = 9;
+
+ private final @ResultCode int mResultCode;
+ @Nullable private final ValueType mResultValue;
+ @Nullable private final String mErrorMessage;
+
+ private AppSearchResult(
+ @ResultCode int resultCode,
+ @Nullable ValueType resultValue,
+ @Nullable String errorMessage) {
+ mResultCode = resultCode;
+ mResultValue = resultValue;
+ mErrorMessage = errorMessage;
+ }
+
+ /** Returns {@code true} if {@link #getResultCode} equals {@link AppSearchResult#RESULT_OK}. */
+ public boolean isSuccess() {
+ return getResultCode() == RESULT_OK;
+ }
+
+ /** Returns one of the {@code RESULT} constants defined in {@link AppSearchResult}. */
+ @ResultCode
+ public int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Returns the result value associated with this result, if it was successful.
+ *
+ * <p>See the documentation of the particular {@link AppSearchSession} call producing this
+ * {@link AppSearchResult} for what is placed in the result value by that call.
+ *
+ * @throws IllegalStateException if this {@link AppSearchResult} is not successful.
+ */
+ @Nullable
+ public ValueType getResultValue() {
+ if (!isSuccess()) {
+ throw new IllegalStateException("AppSearchResult is a failure: " + this);
+ }
+ return mResultValue;
+ }
+
+ /**
+ * Returns the error message associated with this result.
+ *
+ * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. The error
+ * message may be {@code null} even if {@link #isSuccess} is {@code false}. See the
+ * documentation of the particular {@link AppSearchSession} call producing this {@link
+ * AppSearchResult} for what is returned by {@link #getErrorMessage}.
+ */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AppSearchResult)) {
+ return false;
+ }
+ AppSearchResult<?> otherResult = (AppSearchResult<?>) other;
+ return mResultCode == otherResult.mResultCode
+ && Objects.equals(mResultValue, otherResult.mResultValue)
+ && Objects.equals(mErrorMessage, otherResult.mErrorMessage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mResultCode, mResultValue, mErrorMessage);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ if (isSuccess()) {
+ return "[SUCCESS]: " + mResultValue;
+ }
+ return "[FAILURE(" + mResultCode + ")]: " + mErrorMessage;
+ }
+
+ /**
+ * Creates a new successful {@link AppSearchResult}.
+ *
+ * @param value An optional value to associate with the successful result of the operation being
+ * performed.
+ */
+ @NonNull
+ public static <ValueType> AppSearchResult<ValueType> newSuccessfulResult(
+ @Nullable ValueType value) {
+ return new AppSearchResult<>(RESULT_OK, value, /*errorMessage=*/ null);
+ }
+
+ /**
+ * Creates a new failed {@link AppSearchResult}.
+ *
+ * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+ * @param errorMessage An optional string describing the reason or nature of the failure.
+ */
+ @NonNull
+ public static <ValueType> AppSearchResult<ValueType> newFailedResult(
+ @ResultCode int resultCode, @Nullable String errorMessage) {
+ return new AppSearchResult<>(resultCode, /*resultValue=*/ null, errorMessage);
+ }
+
+ /**
+ * Creates a new failed {@link AppSearchResult} by a AppSearchResult in another type.
+ *
+ * @hide
+ */
+ @NonNull
+ public static <ValueType> AppSearchResult<ValueType> newFailedResult(
+ @NonNull AppSearchResult<?> otherFailedResult) {
+ Preconditions.checkState(
+ !otherFailedResult.isSuccess(),
+ "Cannot convert a success result to a failed result");
+ return AppSearchResult.newFailedResult(
+ otherFailedResult.getResultCode(), otherFailedResult.getErrorMessage());
+ }
+
+ /** @hide */
+ @NonNull
+ public static <ValueType> AppSearchResult<ValueType> throwableToFailedResult(
+ @NonNull Throwable t) {
+ // Log for traceability. NOT_FOUND is logged at VERBOSE because this error can occur during
+ // the regular operation of the system (b/183550974). Everything else is indicative of an
+ // actual problem and is logged at WARN.
+ if (t instanceof AppSearchException
+ && ((AppSearchException) t).getResultCode() == RESULT_NOT_FOUND) {
+ if (LogUtil.DEBUG) {
+ Log.v(TAG, "Converting throwable to failed result: " + t);
+ }
+ } else {
+ Log.w(TAG, "Converting throwable to failed result.", t);
+ }
+
+ if (t instanceof AppSearchException) {
+ return ((AppSearchException) t).toAppSearchResult();
+ }
+
+ String exceptionClass = t.getClass().getSimpleName();
+ @AppSearchResult.ResultCode int resultCode;
+ if (t instanceof IllegalStateException || t instanceof NullPointerException) {
+ resultCode = AppSearchResult.RESULT_INTERNAL_ERROR;
+ } else if (t instanceof IllegalArgumentException) {
+ resultCode = AppSearchResult.RESULT_INVALID_ARGUMENT;
+ } else if (t instanceof IOException) {
+ resultCode = AppSearchResult.RESULT_IO_ERROR;
+ } else if (t instanceof SecurityException) {
+ resultCode = AppSearchResult.RESULT_SECURITY_ERROR;
+ } else {
+ resultCode = AppSearchResult.RESULT_UNKNOWN_ERROR;
+ }
+ return AppSearchResult.newFailedResult(resultCode, exceptionClass + ": " + t.getMessage());
+ }
+}
diff --git a/android-34/android/app/appsearch/AppSearchSchema.java b/android-34/android/app/appsearch/AppSearchSchema.java
new file mode 100644
index 0000000..3483d98
--- /dev/null
+++ b/android-34/android/app/appsearch/AppSearchSchema.java
@@ -0,0 +1,1158 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.exceptions.IllegalSchemaException;
+import android.app.appsearch.util.BundleUtil;
+import android.app.appsearch.util.IndentingStringBuilder;
+import android.os.Bundle;
+import android.util.ArraySet;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * The AppSearch Schema for a particular type of document.
+ *
+ * <p>For example, an e-mail message or a music recording could be a schema type.
+ *
+ * <p>The schema consists of type information, properties, and config (like tokenization type).
+ *
+ * @see AppSearchSession#setSchema
+ */
+public final class AppSearchSchema {
+ private static final String SCHEMA_TYPE_FIELD = "schemaType";
+ private static final String PROPERTIES_FIELD = "properties";
+
+ private final Bundle mBundle;
+
+ /** @hide */
+ public AppSearchSchema(@NonNull Bundle bundle) {
+ Objects.requireNonNull(bundle);
+ mBundle = bundle;
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+ appendAppSearchSchemaString(stringBuilder);
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Appends a debugging string for the {@link AppSearchSchema} instance to the given string
+ * builder.
+ *
+ * @param builder the builder to append to.
+ */
+ private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) {
+ Objects.requireNonNull(builder);
+
+ builder.append("{\n");
+ builder.increaseIndentLevel();
+ builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+ builder.append("properties: [\n");
+
+ AppSearchSchema.PropertyConfig[] sortedProperties =
+ getProperties().toArray(new AppSearchSchema.PropertyConfig[0]);
+ Arrays.sort(sortedProperties, (o1, o2) -> o1.getName().compareTo(o2.getName()));
+
+ for (int i = 0; i < sortedProperties.length; i++) {
+ AppSearchSchema.PropertyConfig propertyConfig = sortedProperties[i];
+ builder.increaseIndentLevel();
+ propertyConfig.appendPropertyConfigString(builder);
+ if (i != sortedProperties.length - 1) {
+ builder.append(",\n");
+ }
+ builder.decreaseIndentLevel();
+ }
+
+ builder.append("\n");
+ builder.append("]\n");
+ builder.decreaseIndentLevel();
+ builder.append("}");
+ }
+
+ /** Returns the name of this schema type, e.g. Email. */
+ @NonNull
+ public String getSchemaType() {
+ return mBundle.getString(SCHEMA_TYPE_FIELD, "");
+ }
+
+ /**
+ * Returns the list of {@link PropertyConfig}s that are part of this schema.
+ *
+ * <p>This method creates a new list when called.
+ */
+ @NonNull
+ @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"})
+ public List<PropertyConfig> getProperties() {
+ ArrayList<Bundle> propertyBundles =
+ mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD);
+ if (propertyBundles == null || propertyBundles.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size());
+ for (int i = 0; i < propertyBundles.size(); i++) {
+ ret.add(PropertyConfig.fromBundle(propertyBundles.get(i)));
+ }
+ return ret;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AppSearchSchema)) {
+ return false;
+ }
+ AppSearchSchema otherSchema = (AppSearchSchema) other;
+ if (!getSchemaType().equals(otherSchema.getSchemaType())) {
+ return false;
+ }
+ return getProperties().equals(otherSchema.getProperties());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getSchemaType(), getProperties());
+ }
+
+ /** Builder for {@link AppSearchSchema objects}. */
+ public static final class Builder {
+ private final String mSchemaType;
+ private ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
+ private final Set<String> mPropertyNames = new ArraySet<>();
+ private boolean mBuilt = false;
+
+ /** Creates a new {@link AppSearchSchema.Builder}. */
+ public Builder(@NonNull String schemaType) {
+ Objects.requireNonNull(schemaType);
+ mSchemaType = schemaType;
+ }
+
+ /** Adds a property to the given type. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) {
+ Objects.requireNonNull(propertyConfig);
+ resetIfBuilt();
+ String name = propertyConfig.getName();
+ if (!mPropertyNames.add(name)) {
+ throw new IllegalSchemaException("Property defined more than once: " + name);
+ }
+ mPropertyBundles.add(propertyConfig.mBundle);
+ return this;
+ }
+
+ /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */
+ @NonNull
+ public AppSearchSchema build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
+ bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
+ mBuilt = true;
+ return new AppSearchSchema(bundle);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mPropertyBundles = new ArrayList<>(mPropertyBundles);
+ mBuilt = false;
+ }
+ }
+ }
+
+ /**
+ * Common configuration for a single property (field) in a Document.
+ *
+ * <p>For example, an {@code EmailMessage} would be a type and the {@code subject} would be a
+ * property.
+ */
+ public abstract static class PropertyConfig {
+ static final String NAME_FIELD = "name";
+ static final String DATA_TYPE_FIELD = "dataType";
+ static final String CARDINALITY_FIELD = "cardinality";
+
+ /**
+ * Physical data-types of the contents of the property.
+ *
+ * <p>NOTE: The integer values of these constants must match the proto enum constants in
+ * com.google.android.icing.proto.PropertyConfigProto.DataType.Code.
+ *
+ * @hide
+ */
+ @IntDef(
+ value = {
+ DATA_TYPE_STRING,
+ DATA_TYPE_LONG,
+ DATA_TYPE_DOUBLE,
+ DATA_TYPE_BOOLEAN,
+ DATA_TYPE_BYTES,
+ DATA_TYPE_DOCUMENT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DataType {}
+
+ /** @hide */
+ public static final int DATA_TYPE_STRING = 1;
+
+ /** @hide */
+ public static final int DATA_TYPE_LONG = 2;
+
+ /** @hide */
+ public static final int DATA_TYPE_DOUBLE = 3;
+
+ /** @hide */
+ public static final int DATA_TYPE_BOOLEAN = 4;
+
+ /**
+ * Unstructured BLOB.
+ *
+ * @hide
+ */
+ public static final int DATA_TYPE_BYTES = 5;
+
+ /**
+ * Indicates that the property is itself a {@link GenericDocument}, making it part of a
+ * hierarchical schema. Any property using this DataType MUST have a valid {@link
+ * PropertyConfig#getSchemaType}.
+ *
+ * @hide
+ */
+ public static final int DATA_TYPE_DOCUMENT = 6;
+
+ /**
+ * The cardinality of the property (whether it is required, optional or repeated).
+ *
+ * <p>NOTE: The integer values of these constants must match the proto enum constants in
+ * com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code.
+ *
+ * @hide
+ */
+ @IntDef(
+ value = {
+ CARDINALITY_REPEATED,
+ CARDINALITY_OPTIONAL,
+ CARDINALITY_REQUIRED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Cardinality {}
+
+ /** Any number of items (including zero) [0...*]. */
+ public static final int CARDINALITY_REPEATED = 1;
+
+ /** Zero or one value [0,1]. */
+ public static final int CARDINALITY_OPTIONAL = 2;
+
+ /** Exactly one value [1]. */
+ public static final int CARDINALITY_REQUIRED = 3;
+
+ final Bundle mBundle;
+
+ @Nullable private Integer mHashCode;
+
+ PropertyConfig(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+ appendPropertyConfigString(stringBuilder);
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the
+ * given string builder.
+ *
+ * @param builder the builder to append to.
+ */
+ void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) {
+ Objects.requireNonNull(builder);
+
+ builder.append("{\n");
+ builder.increaseIndentLevel();
+ builder.append("name: \"").append(getName()).append("\",\n");
+
+ if (this instanceof AppSearchSchema.StringPropertyConfig) {
+ ((StringPropertyConfig) this).appendStringPropertyConfigFields(builder);
+ } else if (this instanceof AppSearchSchema.DocumentPropertyConfig) {
+ ((DocumentPropertyConfig) this).appendDocumentPropertyConfigFields(builder);
+ } else if (this instanceof AppSearchSchema.LongPropertyConfig) {
+ ((LongPropertyConfig) this).appendLongPropertyConfigFields(builder);
+ }
+
+ switch (getCardinality()) {
+ case AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED:
+ builder.append("cardinality: CARDINALITY_REPEATED,\n");
+ break;
+ case AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL:
+ builder.append("cardinality: CARDINALITY_OPTIONAL,\n");
+ break;
+ case AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED:
+ builder.append("cardinality: CARDINALITY_REQUIRED,\n");
+ break;
+ default:
+ builder.append("cardinality: CARDINALITY_UNKNOWN,\n");
+ }
+
+ switch (getDataType()) {
+ case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING:
+ builder.append("dataType: DATA_TYPE_STRING,\n");
+ break;
+ case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG:
+ builder.append("dataType: DATA_TYPE_LONG,\n");
+ break;
+ case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE:
+ builder.append("dataType: DATA_TYPE_DOUBLE,\n");
+ break;
+ case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN:
+ builder.append("dataType: DATA_TYPE_BOOLEAN,\n");
+ break;
+ case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES:
+ builder.append("dataType: DATA_TYPE_BYTES,\n");
+ break;
+ case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
+ builder.append("dataType: DATA_TYPE_DOCUMENT,\n");
+ break;
+ default:
+ builder.append("dataType: DATA_TYPE_UNKNOWN,\n");
+ }
+ builder.decreaseIndentLevel();
+ builder.append("}");
+ }
+
+ /** Returns the name of this property. */
+ @NonNull
+ public String getName() {
+ return mBundle.getString(NAME_FIELD, "");
+ }
+
+ /**
+ * Returns the type of data the property contains (e.g. string, int, bytes, etc).
+ *
+ * @hide
+ */
+ @DataType
+ public int getDataType() {
+ return mBundle.getInt(DATA_TYPE_FIELD, -1);
+ }
+
+ /**
+ * Returns the cardinality of the property (whether it is optional, required or repeated).
+ */
+ @Cardinality
+ public int getCardinality() {
+ return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PropertyConfig)) {
+ return false;
+ }
+ PropertyConfig otherProperty = (PropertyConfig) other;
+ return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode = BundleUtil.deepHashCode(mBundle);
+ }
+ return mHashCode;
+ }
+
+ /**
+ * Converts a {@link Bundle} into a {@link PropertyConfig} depending on its internal data
+ * type.
+ *
+ * <p>The bundle is not cloned.
+ *
+ * @throws IllegalArgumentException if the bundle does no contain a recognized value in its
+ * {@code DATA_TYPE_FIELD}.
+ * @hide
+ */
+ @NonNull
+ public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) {
+ switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) {
+ case PropertyConfig.DATA_TYPE_STRING:
+ return new StringPropertyConfig(propertyBundle);
+ case PropertyConfig.DATA_TYPE_LONG:
+ return new LongPropertyConfig(propertyBundle);
+ case PropertyConfig.DATA_TYPE_DOUBLE:
+ return new DoublePropertyConfig(propertyBundle);
+ case PropertyConfig.DATA_TYPE_BOOLEAN:
+ return new BooleanPropertyConfig(propertyBundle);
+ case PropertyConfig.DATA_TYPE_BYTES:
+ return new BytesPropertyConfig(propertyBundle);
+ case PropertyConfig.DATA_TYPE_DOCUMENT:
+ return new DocumentPropertyConfig(propertyBundle);
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported property bundle of type "
+ + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)
+ + "; contents: "
+ + propertyBundle);
+ }
+ }
+ }
+
+ /** Configuration for a property of type String in a Document. */
+ public static final class StringPropertyConfig extends PropertyConfig {
+ private static final String INDEXING_TYPE_FIELD = "indexingType";
+ private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
+ private static final String JOINABLE_VALUE_TYPE_FIELD = "joinableValueType";
+ private static final String DELETION_PROPAGATION_FIELD = "deletionPropagation";
+
+ /**
+ * Encapsulates the configurations on how AppSearch should query/index these terms.
+ *
+ * @hide
+ */
+ @IntDef(
+ value = {
+ INDEXING_TYPE_NONE,
+ INDEXING_TYPE_EXACT_TERMS,
+ INDEXING_TYPE_PREFIXES,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface IndexingType {}
+
+ /** Content in this property will not be tokenized or indexed. */
+ public static final int INDEXING_TYPE_NONE = 0;
+
+ /**
+ * Content in this property should only be returned for queries matching the exact tokens
+ * appearing in this property.
+ *
+ * <p>Ex. A property with "fool" should NOT match a query for "foo".
+ */
+ public static final int INDEXING_TYPE_EXACT_TERMS = 1;
+
+ /**
+ * Content in this property should be returned for queries that are either exact matches or
+ * query matches of the tokens appearing in this property.
+ *
+ * <p>Ex. A property with "fool" <b>should</b> match a query for "foo".
+ */
+ public static final int INDEXING_TYPE_PREFIXES = 2;
+
+ /**
+ * Configures how tokens should be extracted from this property.
+ *
+ * <p>NOTE: The integer values of these constants must match the proto enum constants in
+ * com.google.android.icing.proto.IndexingConfig.TokenizerType.Code.
+ *
+ * @hide
+ */
+ @IntDef(
+ value = {
+ TOKENIZER_TYPE_NONE,
+ TOKENIZER_TYPE_PLAIN,
+ TOKENIZER_TYPE_VERBATIM,
+ TOKENIZER_TYPE_RFC822
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TokenizerType {}
+
+ /**
+ * This value indicates that no tokens should be extracted from this property.
+ *
+ * <p>It is only valid for tokenizer_type to be 'NONE' if {@link #getIndexingType} is {@link
+ * #INDEXING_TYPE_NONE}.
+ */
+ public static final int TOKENIZER_TYPE_NONE = 0;
+
+ /**
+ * Tokenization for plain text. This value indicates that tokens should be extracted from
+ * this property based on word breaks. Segments of whitespace and punctuation are not
+ * considered tokens.
+ *
+ * <p>Ex. A property with "foo bar. baz." will produce tokens for "foo", "bar" and "baz".
+ * The segments " " and "." will not be considered tokens.
+ *
+ * <p>It is only valid for tokenizer_type to be 'PLAIN' if {@link #getIndexingType} is
+ * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
+ */
+ public static final int TOKENIZER_TYPE_PLAIN = 1;
+
+ /**
+ * This value indicates that no normalization or segmentation should be applied to string
+ * values that are tokenized using this type. Therefore, the output token is equivalent to
+ * the raw string value.
+ *
+ * <p>Ex. A property with "Hello, world!" will produce the token "Hello, world!", preserving
+ * punctuation and capitalization, and not creating separate tokens between the space.
+ *
+ * <p>It is only valid for tokenizer_type to be 'VERBATIM' if {@link #getIndexingType} is
+ * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
+ */
+ public static final int TOKENIZER_TYPE_VERBATIM = 2;
+
+ /**
+ * Tokenization for emails. This value indicates that tokens should be extracted from this
+ * property based on email structure.
+ *
+ * <p>Ex. A property with "alex.sav@google.com" will produce tokens for "alex", "sav",
+ * "alex.sav", "google", "com", and "alexsav@google.com"
+ *
+ * <p>It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is
+ * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
+ */
+ public static final int TOKENIZER_TYPE_RFC822 = 3;
+
+ /**
+ * The joinable value type of the property. By setting the appropriate joinable value type
+ * for a property, the client can use the property for joining documents from other schema
+ * types using Search API (see {@link JoinSpec}).
+ *
+ * @hide
+ */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // com.google.android.icing.proto.JoinableConfig.ValueType.Code.
+ @IntDef(
+ value = {
+ JOINABLE_VALUE_TYPE_NONE,
+ JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface JoinableValueType {}
+
+ /** Content in this property is not joinable. */
+ public static final int JOINABLE_VALUE_TYPE_NONE = 0;
+
+ /**
+ * Content in this string property will be used as a qualified id to join documents.
+ *
+ * <ul>
+ * <li>Qualified id: a unique identifier for a document, and this joinable value type is
+ * similar to primary and foreign key in relational database. See {@link
+ * android.app.appsearch.util.DocumentIdUtil} for more details.
+ * <li>Currently we only support single string joining, so it should only be used with
+ * {@link PropertyConfig#CARDINALITY_OPTIONAL} and {@link
+ * PropertyConfig#CARDINALITY_REQUIRED}.
+ * </ul>
+ */
+ public static final int JOINABLE_VALUE_TYPE_QUALIFIED_ID = 1;
+
+ StringPropertyConfig(@NonNull Bundle bundle) {
+ super(bundle);
+ }
+
+ /** Returns how the property is indexed. */
+ @IndexingType
+ public int getIndexingType() {
+ return mBundle.getInt(INDEXING_TYPE_FIELD);
+ }
+
+ /** Returns how this property is tokenized (split into words). */
+ @TokenizerType
+ public int getTokenizerType() {
+ return mBundle.getInt(TOKENIZER_TYPE_FIELD);
+ }
+
+ /**
+ * Returns how this property is going to be used to join documents from other schema types.
+ */
+ @JoinableValueType
+ public int getJoinableValueType() {
+ return mBundle.getInt(JOINABLE_VALUE_TYPE_FIELD, JOINABLE_VALUE_TYPE_NONE);
+ }
+
+ /**
+ * Returns whether or not documents in this schema should be deleted when the document
+ * referenced by this field is deleted.
+ *
+ * @see JoinSpec
+ * @hide
+ */
+ public boolean getDeletionPropagation() {
+ return mBundle.getBoolean(DELETION_PROPAGATION_FIELD, false);
+ }
+
+ /** Builder for {@link StringPropertyConfig}. */
+ public static final class Builder {
+ private final String mPropertyName;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+ @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
+ @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE;
+ @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;
+ private boolean mDeletionPropagation = false;
+
+ /** Creates a new {@link StringPropertyConfig.Builder}. */
+ public Builder(@NonNull String propertyName) {
+ mPropertyName = Objects.requireNonNull(propertyName);
+ }
+
+ /**
+ * The cardinality of the property (whether it is optional, required or repeated).
+ *
+ * <p>If this method is not called, the default cardinality is {@link
+ * PropertyConfig#CARDINALITY_OPTIONAL}.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public StringPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+ Preconditions.checkArgumentInRange(
+ cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+ mCardinality = cardinality;
+ return this;
+ }
+
+ /**
+ * Configures how a property should be indexed so that it can be retrieved by queries.
+ *
+ * <p>If this method is not called, the default indexing type is {@link
+ * StringPropertyConfig#INDEXING_TYPE_NONE}, so that it cannot be matched by queries.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+ Preconditions.checkArgumentInRange(
+ indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
+ mIndexingType = indexingType;
+ return this;
+ }
+
+ /**
+ * Configures how this property should be tokenized (split into words).
+ *
+ * <p>If this method is not called, the default indexing type is {@link
+ * StringPropertyConfig#TOKENIZER_TYPE_NONE}, so that it is not tokenized.
+ *
+ * <p>This method must be called with a value other than {@link
+ * StringPropertyConfig#TOKENIZER_TYPE_NONE} if the property is indexed (i.e. if {@link
+ * #setIndexingType} has been called with a value other than {@link
+ * StringPropertyConfig#INDEXING_TYPE_NONE}).
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
+ Preconditions.checkArgumentInRange(
+ tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_RFC822, "tokenizerType");
+ mTokenizerType = tokenizerType;
+ return this;
+ }
+
+ /**
+ * Configures how this property should be used as a joining matcher.
+ *
+ * <p>If this method is not called, the default joinable value type is {@link
+ * StringPropertyConfig#JOINABLE_VALUE_TYPE_NONE}, so that it is not joinable.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public StringPropertyConfig.Builder setJoinableValueType(
+ @JoinableValueType int joinableValueType) {
+ Preconditions.checkArgumentInRange(
+ joinableValueType,
+ JOINABLE_VALUE_TYPE_NONE,
+ JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+ "joinableValueType");
+ mJoinableValueType = joinableValueType;
+ return this;
+ }
+
+ /**
+ * Configures whether or not documents in this schema will be removed when the document
+ * referred to by this property is deleted.
+ *
+ * <p>Requires that a joinable value type is set.
+ *
+ * @hide
+ */
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getDeletionPropagation
+ @NonNull
+ public Builder setDeletionPropagation(boolean deletionPropagation) {
+ mDeletionPropagation = deletionPropagation;
+ return this;
+ }
+
+ /** Constructs a new {@link StringPropertyConfig} from the contents of this builder. */
+ @NonNull
+ public StringPropertyConfig build() {
+ if (mTokenizerType == TOKENIZER_TYPE_NONE) {
+ Preconditions.checkState(
+ mIndexingType == INDEXING_TYPE_NONE,
+ "Cannot set "
+ + "TOKENIZER_TYPE_NONE with an indexing type other than "
+ + "INDEXING_TYPE_NONE.");
+ } else {
+ Preconditions.checkState(
+ mIndexingType != INDEXING_TYPE_NONE,
+ "Cannot set " + "TOKENIZER_TYPE_PLAIN with INDEXING_TYPE_NONE.");
+ }
+ if (mJoinableValueType == JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
+ Preconditions.checkState(
+ mCardinality != CARDINALITY_REPEATED,
+ "Cannot set JOINABLE_VALUE_TYPE_QUALIFIED_ID with"
+ + " CARDINALITY_REPEATED.");
+ } else {
+ Preconditions.checkState(
+ !mDeletionPropagation,
+ "Cannot set deletion "
+ + "propagation without setting a joinable value type");
+ }
+ Bundle bundle = new Bundle();
+ bundle.putString(NAME_FIELD, mPropertyName);
+ bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING);
+ bundle.putInt(CARDINALITY_FIELD, mCardinality);
+ bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
+ bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType);
+ bundle.putInt(JOINABLE_VALUE_TYPE_FIELD, mJoinableValueType);
+ bundle.putBoolean(DELETION_PROPAGATION_FIELD, mDeletionPropagation);
+ return new StringPropertyConfig(bundle);
+ }
+ }
+
+ /**
+ * Appends a debug string for the {@link StringPropertyConfig} instance to the given string
+ * builder.
+ *
+ * <p>This appends fields specific to a {@link StringPropertyConfig} instance.
+ *
+ * @param builder the builder to append to.
+ */
+ void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
+ switch (getIndexingType()) {
+ case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE:
+ builder.append("indexingType: INDEXING_TYPE_NONE,\n");
+ break;
+ case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS:
+ builder.append("indexingType: INDEXING_TYPE_EXACT_TERMS,\n");
+ break;
+ case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES:
+ builder.append("indexingType: INDEXING_TYPE_PREFIXES,\n");
+ break;
+ default:
+ builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n");
+ }
+
+ switch (getTokenizerType()) {
+ case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE:
+ builder.append("tokenizerType: TOKENIZER_TYPE_NONE,\n");
+ break;
+ case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN:
+ builder.append("tokenizerType: TOKENIZER_TYPE_PLAIN,\n");
+ break;
+ case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_VERBATIM:
+ builder.append("tokenizerType: TOKENIZER_TYPE_VERBATIM,\n");
+ break;
+ case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822:
+ builder.append("tokenizerType: TOKENIZER_TYPE_RFC822,\n");
+ break;
+ default:
+ builder.append("tokenizerType: TOKENIZER_TYPE_UNKNOWN,\n");
+ }
+
+ switch (getJoinableValueType()) {
+ case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE:
+ builder.append("joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n");
+ break;
+ case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID:
+ builder.append("joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n");
+ break;
+ default:
+ builder.append("joinableValueType: JOINABLE_VALUE_TYPE_UNKNOWN,\n");
+ }
+ }
+ }
+
+ /** Configuration for a property containing a 64-bit integer. */
+ public static final class LongPropertyConfig extends PropertyConfig {
+ private static final String INDEXING_TYPE_FIELD = "indexingType";
+
+ /**
+ * Encapsulates the configurations on how AppSearch should query/index these 64-bit
+ * integers.
+ *
+ * @hide
+ */
+ @IntDef(value = {INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface IndexingType {}
+
+ /** Content in this property will not be indexed. */
+ public static final int INDEXING_TYPE_NONE = 0;
+
+ /**
+ * Content in this property will be indexed and can be fetched via numeric search range
+ * query.
+ *
+ * <p>Ex. A property with 1024 should match numeric search range query [0, 2000].
+ */
+ public static final int INDEXING_TYPE_RANGE = 1;
+
+ LongPropertyConfig(@NonNull Bundle bundle) {
+ super(bundle);
+ }
+
+ /** Returns how the property is indexed. */
+ @IndexingType
+ public int getIndexingType() {
+ return mBundle.getInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE);
+ }
+
+ /** Builder for {@link LongPropertyConfig}. */
+ public static final class Builder {
+ private final String mPropertyName;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+ @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
+
+ /** Creates a new {@link LongPropertyConfig.Builder}. */
+ public Builder(@NonNull String propertyName) {
+ mPropertyName = Objects.requireNonNull(propertyName);
+ }
+
+ /**
+ * The cardinality of the property (whether it is optional, required or repeated).
+ *
+ * <p>If this method is not called, the default cardinality is {@link
+ * PropertyConfig#CARDINALITY_OPTIONAL}.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public LongPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+ Preconditions.checkArgumentInRange(
+ cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+ mCardinality = cardinality;
+ return this;
+ }
+
+ /**
+ * Configures how a property should be indexed so that it can be retrieved by queries.
+ *
+ * <p>If this method is not called, the default indexing type is {@link
+ * LongPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed and cannot be
+ * matched by queries.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+ Preconditions.checkArgumentInRange(
+ indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE, "indexingType");
+ mIndexingType = indexingType;
+ return this;
+ }
+
+ /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */
+ @NonNull
+ public LongPropertyConfig build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(NAME_FIELD, mPropertyName);
+ bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG);
+ bundle.putInt(CARDINALITY_FIELD, mCardinality);
+ bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
+ return new LongPropertyConfig(bundle);
+ }
+ }
+
+ /**
+ * Appends a debug string for the {@link LongPropertyConfig} instance to the given string
+ * builder.
+ *
+ * <p>This appends fields specific to a {@link LongPropertyConfig} instance.
+ *
+ * @param builder the builder to append to.
+ */
+ void appendLongPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
+ switch (getIndexingType()) {
+ case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE:
+ builder.append("indexingType: INDEXING_TYPE_NONE,\n");
+ break;
+ case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE:
+ builder.append("indexingType: INDEXING_TYPE_RANGE,\n");
+ break;
+ default:
+ builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n");
+ }
+ }
+ }
+
+ /** Configuration for a property containing a double-precision decimal number. */
+ public static final class DoublePropertyConfig extends PropertyConfig {
+ DoublePropertyConfig(@NonNull Bundle bundle) {
+ super(bundle);
+ }
+
+ /** Builder for {@link DoublePropertyConfig}. */
+ public static final class Builder {
+ private final String mPropertyName;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+
+ /** Creates a new {@link DoublePropertyConfig.Builder}. */
+ public Builder(@NonNull String propertyName) {
+ mPropertyName = Objects.requireNonNull(propertyName);
+ }
+
+ /**
+ * The cardinality of the property (whether it is optional, required or repeated).
+ *
+ * <p>If this method is not called, the default cardinality is {@link
+ * PropertyConfig#CARDINALITY_OPTIONAL}.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public DoublePropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+ Preconditions.checkArgumentInRange(
+ cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+ mCardinality = cardinality;
+ return this;
+ }
+
+ /** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */
+ @NonNull
+ public DoublePropertyConfig build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(NAME_FIELD, mPropertyName);
+ bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE);
+ bundle.putInt(CARDINALITY_FIELD, mCardinality);
+ return new DoublePropertyConfig(bundle);
+ }
+ }
+ }
+
+ /** Configuration for a property containing a boolean. */
+ public static final class BooleanPropertyConfig extends PropertyConfig {
+ BooleanPropertyConfig(@NonNull Bundle bundle) {
+ super(bundle);
+ }
+
+ /** Builder for {@link BooleanPropertyConfig}. */
+ public static final class Builder {
+ private final String mPropertyName;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+
+ /** Creates a new {@link BooleanPropertyConfig.Builder}. */
+ public Builder(@NonNull String propertyName) {
+ mPropertyName = Objects.requireNonNull(propertyName);
+ }
+
+ /**
+ * The cardinality of the property (whether it is optional, required or repeated).
+ *
+ * <p>If this method is not called, the default cardinality is {@link
+ * PropertyConfig#CARDINALITY_OPTIONAL}.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public BooleanPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+ Preconditions.checkArgumentInRange(
+ cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+ mCardinality = cardinality;
+ return this;
+ }
+
+ /** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */
+ @NonNull
+ public BooleanPropertyConfig build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(NAME_FIELD, mPropertyName);
+ bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN);
+ bundle.putInt(CARDINALITY_FIELD, mCardinality);
+ return new BooleanPropertyConfig(bundle);
+ }
+ }
+ }
+
+ /** Configuration for a property containing a byte array. */
+ public static final class BytesPropertyConfig extends PropertyConfig {
+ BytesPropertyConfig(@NonNull Bundle bundle) {
+ super(bundle);
+ }
+
+ /** Builder for {@link BytesPropertyConfig}. */
+ public static final class Builder {
+ private final String mPropertyName;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+
+ /** Creates a new {@link BytesPropertyConfig.Builder}. */
+ public Builder(@NonNull String propertyName) {
+ mPropertyName = Objects.requireNonNull(propertyName);
+ }
+
+ /**
+ * The cardinality of the property (whether it is optional, required or repeated).
+ *
+ * <p>If this method is not called, the default cardinality is {@link
+ * PropertyConfig#CARDINALITY_OPTIONAL}.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public BytesPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+ Preconditions.checkArgumentInRange(
+ cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+ mCardinality = cardinality;
+ return this;
+ }
+
+ /** Constructs a new {@link BytesPropertyConfig} from the contents of this builder. */
+ @NonNull
+ public BytesPropertyConfig build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(NAME_FIELD, mPropertyName);
+ bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES);
+ bundle.putInt(CARDINALITY_FIELD, mCardinality);
+ return new BytesPropertyConfig(bundle);
+ }
+ }
+ }
+
+ /** Configuration for a property containing another Document. */
+ public static final class DocumentPropertyConfig extends PropertyConfig {
+ private static final String SCHEMA_TYPE_FIELD = "schemaType";
+ private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties";
+
+ DocumentPropertyConfig(@NonNull Bundle bundle) {
+ super(bundle);
+ }
+
+ /** Returns the logical schema-type of the contents of this document property. */
+ @NonNull
+ public String getSchemaType() {
+ return Objects.requireNonNull(mBundle.getString(SCHEMA_TYPE_FIELD));
+ }
+
+ /**
+ * Returns whether fields in the nested document should be indexed according to that
+ * document's schema.
+ *
+ * <p>If false, the nested document's properties are not indexed regardless of its own
+ * schema.
+ */
+ public boolean shouldIndexNestedProperties() {
+ return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD);
+ }
+
+ /** Builder for {@link DocumentPropertyConfig}. */
+ public static final class Builder {
+ private final String mPropertyName;
+ private final String mSchemaType;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+ private boolean mShouldIndexNestedProperties = false;
+
+ /**
+ * Creates a new {@link DocumentPropertyConfig.Builder}.
+ *
+ * @param propertyName The logical name of the property in the schema, which will be
+ * used as the key for this property in {@link
+ * GenericDocument.Builder#setPropertyDocument}.
+ * @param schemaType The type of documents which will be stored in this property.
+ * Documents of different types cannot be mixed into a single property.
+ */
+ public Builder(@NonNull String propertyName, @NonNull String schemaType) {
+ mPropertyName = Objects.requireNonNull(propertyName);
+ mSchemaType = Objects.requireNonNull(schemaType);
+ }
+
+ /**
+ * The cardinality of the property (whether it is optional, required or repeated).
+ *
+ * <p>If this method is not called, the default cardinality is {@link
+ * PropertyConfig#CARDINALITY_OPTIONAL}.
+ */
+ @CanIgnoreReturnValue
+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+ @NonNull
+ public DocumentPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+ Preconditions.checkArgumentInRange(
+ cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+ mCardinality = cardinality;
+ return this;
+ }
+
+ /**
+ * Configures whether fields in the nested document should be indexed according to that
+ * document's schema.
+ *
+ * <p>If false, the nested document's properties are not indexed regardless of its own
+ * schema.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public DocumentPropertyConfig.Builder setShouldIndexNestedProperties(
+ boolean indexNestedProperties) {
+ mShouldIndexNestedProperties = indexNestedProperties;
+ return this;
+ }
+
+ /** Constructs a new {@link PropertyConfig} from the contents of this builder. */
+ @NonNull
+ public DocumentPropertyConfig build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(NAME_FIELD, mPropertyName);
+ bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT);
+ bundle.putInt(CARDINALITY_FIELD, mCardinality);
+ bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties);
+ bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType);
+ return new DocumentPropertyConfig(bundle);
+ }
+ }
+
+ /**
+ * Appends a debug string for the {@link DocumentPropertyConfig} instance to the given
+ * string builder.
+ *
+ * <p>This appends fields specific to a {@link DocumentPropertyConfig} instance.
+ *
+ * @param builder the builder to append to.
+ */
+ void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
+ builder.append("shouldIndexNestedProperties: ")
+ .append(shouldIndexNestedProperties())
+ .append(",\n");
+
+ builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/AppSearchSession.java b/android-34/android/app/appsearch/AppSearchSession.java
new file mode 100644
index 0000000..c4ed25b
--- /dev/null
+++ b/android-34/android/app/appsearch/AppSearchSession.java
@@ -0,0 +1,1058 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import static android.app.appsearch.SearchSessionUtil.safeExecute;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.app.appsearch.aidl.AppSearchBatchResultParcel;
+import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.DocumentsParcel;
+import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
+import android.app.appsearch.aidl.IAppSearchManager;
+import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.app.appsearch.stats.SchemaMigrationStats;
+import android.app.appsearch.util.SchemaMigrationUtil;
+import android.content.AttributionSource;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Provides a connection to a single AppSearch database.
+ *
+ * <p>An {@link AppSearchSession} instance provides access to database operations such as
+ * setting a schema, adding documents, and searching.
+ *
+ * <p>This class is thread safe.
+ *
+ * @see GlobalSearchSession
+ */
+public final class AppSearchSession implements Closeable {
+ private static final String TAG = "AppSearchSession";
+
+ private final AttributionSource mCallerAttributionSource;
+ private final String mDatabaseName;
+ private final UserHandle mUserHandle;
+ private final IAppSearchManager mService;
+
+ private boolean mIsMutated = false;
+ private boolean mIsClosed = false;
+
+ /**
+ * Creates a search session for the client, defined by the {@code userHandle} and
+ * {@code packageName}.
+ */
+ static void createSearchSession(
+ @NonNull AppSearchManager.SearchContext searchContext,
+ @NonNull IAppSearchManager service,
+ @NonNull UserHandle userHandle,
+ @NonNull AttributionSource callerAttributionSource,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
+ AppSearchSession searchSession =
+ new AppSearchSession(service, userHandle, callerAttributionSource,
+ searchContext.mDatabaseName);
+ searchSession.initialize(executor, callback);
+ }
+
+ // NOTE: No instance of this class should be created or returned except via initialize().
+ // Once the callback.accept has been called here, the class is ready to use.
+ private void initialize(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
+ try {
+ mService.initialize(
+ mCallerAttributionSource,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ AppSearchResult<Void> result = resultParcel.getResult();
+ if (result.isSuccess()) {
+ callback.accept(
+ AppSearchResult.newSuccessfulResult(
+ AppSearchSession.this));
+ } else {
+ callback.accept(AppSearchResult.newFailedResult(result));
+ }
+ });
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private AppSearchSession(@NonNull IAppSearchManager service, @NonNull UserHandle userHandle,
+ @NonNull AttributionSource callerAttributionSource, @NonNull String databaseName) {
+ mService = service;
+ mUserHandle = userHandle;
+ mCallerAttributionSource = callerAttributionSource;
+ mDatabaseName = databaseName;
+ }
+
+ /**
+ * Sets the schema that represents the organizational structure of data within the AppSearch
+ * database.
+ *
+ * <p>Upon creating an {@link AppSearchSession}, {@link #setSchema} should be called. If the
+ * schema needs to be updated, or it has not been previously set, then the provided schema will
+ * be saved and persisted to disk. Otherwise, {@link #setSchema} is handled efficiently as a
+ * no-op call.
+ *
+ * @param request the schema to set or update the AppSearch database to.
+ * @param workExecutor Executor on which to schedule heavy client-side background work such as
+ * transforming documents.
+ * @param callbackExecutor Executor on which to invoke the callback.
+ * @param callback Callback to receive errors resulting from setting the schema. If the
+ * operation succeeds, the callback will be invoked with {@code null}.
+ */
+ public void setSchema(
+ @NonNull SetSchemaRequest request,
+ @NonNull Executor workExecutor,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(workExecutor);
+ Objects.requireNonNull(callbackExecutor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ List<Bundle> schemaBundles = new ArrayList<>(request.getSchemas().size());
+ for (AppSearchSchema schema : request.getSchemas()) {
+ schemaBundles.add(schema.getBundle());
+ }
+
+ // Extract a List<VisibilityDocument> from the request and convert to a
+ // List<VisibilityDocument.Bundle> to send via binder.
+ List<VisibilityDocument> visibilityDocuments = VisibilityDocument
+ .toVisibilityDocuments(request);
+ List<Bundle> visibilityBundles = new ArrayList<>(visibilityDocuments.size());
+ for (int i = 0; i < visibilityDocuments.size(); i++) {
+ visibilityBundles.add(visibilityDocuments.get(i).getBundle());
+ }
+
+ // No need to trigger migration if user never set migrator
+ if (request.getMigrators().isEmpty()) {
+ setSchemaNoMigrations(
+ request,
+ schemaBundles,
+ visibilityBundles,
+ callbackExecutor,
+ callback);
+ } else {
+ setSchemaWithMigrations(
+ request,
+ schemaBundles,
+ visibilityBundles,
+ workExecutor,
+ callbackExecutor,
+ callback);
+ }
+ mIsMutated = true;
+ }
+
+ /**
+ * Retrieves the schema most recently successfully provided to {@link #setSchema}.
+ *
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive the pending results of schema.
+ */
+ public void getSchema(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<GetSchemaResponse>> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ String targetPackageName =
+ Objects.requireNonNull(mCallerAttributionSource.getPackageName());
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ try {
+ mService.getSchema(
+ mCallerAttributionSource,
+ targetPackageName,
+ mDatabaseName,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ AppSearchResult<Bundle> result = resultParcel.getResult();
+ if (result.isSuccess()) {
+ GetSchemaResponse response = new GetSchemaResponse(
+ Objects.requireNonNull(result.getResultValue()));
+ callback.accept(AppSearchResult.newSuccessfulResult(response));
+ } else {
+ callback.accept(AppSearchResult.newFailedResult(result));
+ }
+ });
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves the set of all namespaces in the current database with at least one document.
+ *
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive the namespaces.
+ */
+ public void getNamespaces(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<Set<String>>> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ try {
+ mService.getNamespaces(
+ mCallerAttributionSource,
+ mDatabaseName,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ AppSearchResult<List<String>> result = resultParcel.getResult();
+ if (result.isSuccess()) {
+ Set<String> namespaces =
+ new ArraySet<>(result.getResultValue());
+ callback.accept(
+ AppSearchResult.newSuccessfulResult(namespaces));
+ } else {
+ callback.accept(AppSearchResult.newFailedResult(result));
+ }
+ });
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Indexes documents into the {@link AppSearchSession} database.
+ *
+ * <p>Each {@link GenericDocument} object must have a {@code schemaType} field set to an {@link
+ * AppSearchSchema} type that has been previously registered by calling the {@link #setSchema}
+ * method.
+ *
+ * @param request containing documents to be indexed.
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive pending result of performing this operation. The keys
+ * of the returned {@link AppSearchBatchResult} are the IDs of the input
+ * documents. The values are {@code null} if they were successfully indexed,
+ * or a failed {@link AppSearchResult} otherwise. If an unexpected internal
+ * error occurs in the AppSearch service,
+ * {@link BatchResultCallback#onSystemError} will be invoked with a
+ * {@link Throwable}.
+ */
+ public void put(
+ @NonNull PutDocumentsRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BatchResultCallback<String, Void> callback) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ DocumentsParcel documentsParcel =
+ new DocumentsParcel(request.getGenericDocuments());
+ try {
+ mService.putDocuments(mCallerAttributionSource, mDatabaseName, documentsParcel,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchBatchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchBatchResultParcel resultParcel) {
+ safeExecute(
+ executor,
+ callback,
+ () -> callback.onResult(resultParcel.getResult()));
+ }
+
+ @Override
+ public void onSystemError(AppSearchResultParcel resultParcel) {
+ safeExecute(
+ executor,
+ callback,
+ () -> SearchSessionUtil.sendSystemErrorToCallback(
+ resultParcel.getResult(), callback));
+ }
+ });
+ mIsMutated = true;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets {@link GenericDocument} objects by document IDs in a namespace from the {@link
+ * AppSearchSession} database.
+ *
+ * @param request a request containing a namespace and IDs to get documents for.
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive the pending result of performing this operation. The keys
+ * of the returned {@link AppSearchBatchResult} are the input IDs. The values
+ * are the returned {@link GenericDocument}s on success, or a failed
+ * {@link AppSearchResult} otherwise. IDs that are not found will return a
+ * failed {@link AppSearchResult} with a result code of
+ * {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
+ * occurs in the AppSearch service, {@link BatchResultCallback#onSystemError}
+ * will be invoked with a {@link Throwable}.
+ */
+ public void getByDocumentId(
+ @NonNull GetByDocumentIdRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BatchResultCallback<String, GenericDocument> callback) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ String targetPackageName =
+ Objects.requireNonNull(mCallerAttributionSource.getPackageName());
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ try {
+ mService.getDocuments(
+ mCallerAttributionSource,
+ targetPackageName,
+ mDatabaseName,
+ request.getNamespace(),
+ new ArrayList<>(request.getIds()),
+ request.getProjectionsInternal(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ SearchSessionUtil.createGetDocumentCallback(executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves documents from the open {@link AppSearchSession} that match a given query
+ * string and type of search provided.
+ *
+ * <p>Query strings can be empty, contain one term with no operators, or contain multiple terms
+ * and operators.
+ *
+ * <p>For query strings that are empty, all documents that match the {@link SearchSpec} will be
+ * returned.
+ *
+ * <p>For query strings with a single term and no operators, documents that match the provided
+ * query string and {@link SearchSpec} will be returned.
+ *
+ * <p>The following operators are supported:
+ *
+ * <ul>
+ * <li>AND (implicit)
+ * <p>AND is an operator that matches documents that contain <i>all</i> provided terms.
+ * <p><b>NOTE:</b> A space between terms is treated as an "AND" operator. Explicitly
+ * including "AND" in a query string will treat "AND" as a term, returning documents that
+ * also contain "AND".
+ * <p>Example: "apple AND banana" matches documents that contain the terms "apple", "and",
+ * "banana".
+ * <p>Example: "apple banana" matches documents that contain both "apple" and "banana".
+ * <p>Example: "apple banana cherry" matches documents that contain "apple", "banana", and
+ * "cherry".
+ * <li>OR
+ * <p>OR is an operator that matches documents that contain <i>any</i> provided term.
+ * <p>Example: "apple OR banana" matches documents that contain either "apple" or
+ * "banana".
+ * <p>Example: "apple OR banana OR cherry" matches documents that contain any of "apple",
+ * "banana", or "cherry".
+ * <li>Exclusion (-)
+ * <p>Exclusion (-) is an operator that matches documents that <i>do not</i> contain the
+ * provided term.
+ * <p>Example: "-apple" matches documents that do not contain "apple".
+ * <li>Grouped Terms
+ * <p>For queries that require multiple operators and terms, terms can be grouped into
+ * subqueries. Subqueries are contained within an open "(" and close ")" parenthesis.
+ * <p>Example: "(donut OR bagel) (coffee OR tea)" matches documents that contain either
+ * "donut" or "bagel" and either "coffee" or "tea".
+ * <li>Property Restricts
+ * <p>For queries that require a term to match a specific {@link AppSearchSchema} property
+ * of a document, a ":" must be included between the property name and the term.
+ * <p>Example: "subject:important" matches documents that contain the term "important" in
+ * the "subject" property.
+ * </ul>
+ *
+ * <p>Additional search specifications, such as filtering by {@link AppSearchSchema} type or
+ * adding projection, can be set by calling the corresponding {@link SearchSpec.Builder} setter.
+ *
+ * <p>This method is lightweight. The heavy work will be done in {@link
+ * SearchResults#getNextPage}.
+ *
+ * @param queryExpression query string to search.
+ * @param searchSpec spec for setting document filters, adding projection, setting term match
+ * type, etc.
+ * @return a {@link SearchResults} object for retrieved matched documents.
+ */
+ @NonNull
+ public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ Objects.requireNonNull(queryExpression);
+ Objects.requireNonNull(searchSpec);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ return new SearchResults(mService, mCallerAttributionSource, mDatabaseName, queryExpression,
+ searchSpec, mUserHandle);
+ }
+
+ /**
+ * Retrieves suggested Strings that could be used as {@code queryExpression} in
+ * {@link #search(String, SearchSpec)} API.
+ *
+ * <p>The {@code suggestionQueryExpression} can contain one term with no operators, or contain
+ * multiple terms and operators. Operators will be considered as a normal term. Please see the
+ * operator examples below. The {@code suggestionQueryExpression} must end with a valid term,
+ * the suggestions are generated based on the last term. If the input
+ * {@code suggestionQueryExpression} doesn't have a valid token, AppSearch will return an
+ * empty result list. Please see the invalid examples below.
+ *
+ * <p>Example: if there are following documents with content stored in AppSearch.
+ * <ul>
+ * <li>document1: "term1"
+ * <li>document2: "term1 term2"
+ * <li>document3: "term1 term2 term3"
+ * <li>document4: "org"
+ * </ul>
+ *
+ * <p>Search suggestions with the single term {@code suggestionQueryExpression} "t", the
+ * suggested results are:
+ * <ul>
+ * <li>"term1" - Use it to be queryExpression in {@link #search} could get 3
+ * {@link SearchResult}s, which contains document 1, 2 and 3.
+ * <li>"term2" - Use it to be queryExpression in {@link #search} could get 2
+ * {@link SearchResult}s, which contains document 2 and 3.
+ * <li>"term3" - Use it to be queryExpression in {@link #search} could get 1
+ * {@link SearchResult}, which contains document 3.
+ * </ul>
+ *
+ * <p>Search suggestions with the multiple term {@code suggestionQueryExpression} "org t", the
+ * suggested result will be "org term1" - The last token is completed by the suggested
+ * String.
+ *
+ * <p>Operators in {@link #search} are supported.
+ * <p><b>NOTE:</b> Exclusion and Grouped Terms in the last term is not supported.
+ * <p>example: "apple -f": This Api will throw an
+ * {@link android.app.appsearch.exceptions.AppSearchException} with
+ * {@link AppSearchResult#RESULT_INVALID_ARGUMENT}.
+ * <p>example: "apple (f)": This Api will return an empty results.
+ *
+ * <p>Invalid example: All these input {@code suggestionQueryExpression} don't have a valid
+ * last token, AppSearch will return an empty result list.
+ * <ul>
+ * <li>"" - Empty {@code suggestionQueryExpression}.
+ * <li>"(f)" - Ending in a closed brackets.
+ * <li>"f:" - Ending in an operator.
+ * <li>"f " - Ending in trailing space.
+ * </ul>
+ *
+ * @param suggestionQueryExpression the non empty query string to search suggestions
+ * @param searchSuggestionSpec spec for setting document filters
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive the pending result of performing this operation, which
+ * is a List of {@link SearchSuggestionResult} on success. The returned
+ * suggestion Strings are ordered by the number of {@link SearchResult} you
+ * could get by using that suggestion in {@link #search}.
+ *
+ * @see #search(String, SearchSpec)
+ */
+ public void searchSuggestion(
+ @NonNull String suggestionQueryExpression,
+ @NonNull SearchSuggestionSpec searchSuggestionSpec,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<List<SearchSuggestionResult>>> callback) {
+ Objects.requireNonNull(suggestionQueryExpression);
+ Objects.requireNonNull(searchSuggestionSpec);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ try {
+ mService.searchSuggestion(
+ mCallerAttributionSource,
+ mDatabaseName,
+ suggestionQueryExpression,
+ searchSuggestionSpec.getBundle(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ try {
+ AppSearchResult<List<Bundle>> result = resultParcel.getResult();
+ if (result.isSuccess()) {
+ List<Bundle> suggestionResultBundles =
+ result.getResultValue();
+ List<SearchSuggestionResult> searchSuggestionResults =
+ new ArrayList<>(suggestionResultBundles.size());
+ for (int i = 0; i < suggestionResultBundles.size(); i++) {
+ SearchSuggestionResult searchSuggestionResult =
+ new SearchSuggestionResult(
+ suggestionResultBundles.get(i));
+ searchSuggestionResults.add(searchSuggestionResult);
+ }
+ callback.accept(
+ AppSearchResult.newSuccessfulResult(
+ searchSuggestionResults));
+ } else {
+ // TODO(b/261897334) save SDK errors/crashes and send to
+ // server for logging.
+ callback.accept(AppSearchResult.newFailedResult(result));
+ }
+ } catch (Exception e) {
+ callback.accept(AppSearchResult.throwableToFailedResult(e));
+ }
+ });
+ }
+ }
+ );
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Reports usage of a particular document by namespace and ID.
+ *
+ * <p>A usage report represents an event in which a user interacted with or viewed a document.
+ *
+ * <p>For each call to {@link #reportUsage}, AppSearch updates usage count and usage recency
+ * metrics for that particular document. These metrics are used for ordering {@link #search}
+ * results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and {@link
+ * SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} ranking strategies.
+ *
+ * <p>Reporting usage of a document is optional.
+ *
+ * @param request The usage reporting request.
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive errors. If the operation succeeds, the callback will be
+ * invoked with {@code null}.
+ */
+ public void reportUsage(
+ @NonNull ReportUsageRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<Void>> callback) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ String targetPackageName =
+ Objects.requireNonNull(mCallerAttributionSource.getPackageName());
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ try {
+ mService.reportUsage(
+ mCallerAttributionSource,
+ targetPackageName,
+ mDatabaseName,
+ request.getNamespace(),
+ request.getDocumentId(),
+ request.getUsageTimestampMillis(),
+ /*systemUsage=*/ false,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(
+ executor,
+ callback,
+ () -> callback.accept(resultParcel.getResult()));
+ }
+ });
+ mIsMutated = true;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes {@link GenericDocument} objects by document IDs in a namespace from the {@link
+ * AppSearchSession} database.
+ *
+ * <p>Removed documents will no longer be surfaced by {@link #search} or {@link
+ * #getByDocumentId} calls.
+ *
+ * <p>Once the database crosses the document count or byte usage threshold, removed documents
+ * will be deleted from disk.
+ *
+ * @param request {@link RemoveByDocumentIdRequest} with IDs in a namespace to remove from the
+ * index.
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive the pending result of performing this operation. The keys
+ * of the returned {@link AppSearchBatchResult} are the input document IDs. The
+ * values are {@code null} on success, or a failed {@link AppSearchResult}
+ * otherwise. IDs that are not found will return a failed
+ * {@link AppSearchResult} with a result code of
+ * {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
+ * occurs in the AppSearch service, {@link BatchResultCallback#onSystemError}
+ * will be invoked with a {@link Throwable}.
+ */
+ public void remove(
+ @NonNull RemoveByDocumentIdRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BatchResultCallback<String, Void> callback) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ try {
+ mService.removeByDocumentId(
+ mCallerAttributionSource,
+ mDatabaseName,
+ request.getNamespace(),
+ new ArrayList<>(request.getIds()),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchBatchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchBatchResultParcel resultParcel) {
+ safeExecute(
+ executor,
+ callback,
+ () -> callback.onResult(resultParcel.getResult()));
+ }
+
+ @Override
+ public void onSystemError(AppSearchResultParcel resultParcel) {
+ safeExecute(
+ executor,
+ callback,
+ () -> SearchSessionUtil.sendSystemErrorToCallback(
+ resultParcel.getResult(), callback));
+ }
+ });
+ mIsMutated = true;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
+ * match the {@code queryExpression} in given namespaces and schemaTypes which is set via {@link
+ * SearchSpec.Builder#addFilterNamespaces} and {@link SearchSpec.Builder#addFilterSchemas}.
+ *
+ * <p>An empty {@code queryExpression} matches all documents.
+ *
+ * <p>An empty set of namespaces or schemaTypes matches all namespaces or schemaTypes in the
+ * current database.
+ *
+ * @param queryExpression Query String to search.
+ * @param searchSpec Spec containing schemaTypes, namespaces and query expression indicates how
+ * document will be removed. All specific about how to scoring, ordering, snippeting and
+ * resulting will be ignored.
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive errors resulting from removing the documents. If
+ * the operation succeeds, the callback will be invoked with
+ * {@code null}.
+ */
+ public void remove(
+ @NonNull String queryExpression,
+ @NonNull SearchSpec searchSpec,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<Void>> callback) {
+ Objects.requireNonNull(queryExpression);
+ Objects.requireNonNull(searchSpec);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ if (searchSpec.getJoinSpec() != null) {
+ throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+ + "JoinSpec was provided.");
+ }
+ try {
+ mService.removeByQuery(
+ mCallerAttributionSource,
+ mDatabaseName,
+ queryExpression,
+ searchSpec.getBundle(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(
+ executor,
+ callback,
+ () -> callback.accept(resultParcel.getResult()));
+ }
+ });
+ mIsMutated = true;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the storage info for this {@link AppSearchSession} database.
+ *
+ * <p>This may take time proportional to the number of documents and may be inefficient to call
+ * repeatedly.
+ *
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive the storage info.
+ */
+ public void getStorageInfo(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<StorageInfo>> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+ try {
+ mService.getStorageInfo(
+ mCallerAttributionSource,
+ mDatabaseName,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ AppSearchResult<Bundle> result = resultParcel.getResult();
+ if (result.isSuccess()) {
+ StorageInfo response = new StorageInfo(
+ Objects.requireNonNull(result.getResultValue()));
+ callback.accept(AppSearchResult.newSuccessfulResult(response));
+ } else {
+ callback.accept(AppSearchResult.newFailedResult(result));
+ }
+ });
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Closes the {@link AppSearchSession} to persist all schema and document updates,
+ * additions, and deletes to disk.
+ */
+ @Override
+ public void close() {
+ if (mIsMutated && !mIsClosed) {
+ try {
+ mService.persistToDisk(
+ mCallerAttributionSource,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime());
+ mIsClosed = true;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to close the AppSearchSession", e);
+ }
+ }
+ }
+
+ /**
+ * Set schema to Icing for no-migration scenario.
+ *
+ * <p>We only need one time {@link #setSchema} call for no-migration scenario by using the
+ * forceoverride in the request.
+ */
+ private void setSchemaNoMigrations(
+ @NonNull SetSchemaRequest request,
+ @NonNull List<Bundle> schemaBundles,
+ @NonNull List<Bundle> visibilityBundles,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
+ try {
+ mService.setSchema(
+ mCallerAttributionSource,
+ mDatabaseName,
+ schemaBundles,
+ visibilityBundles,
+ request.isForceOverride(),
+ request.getVersion(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ SchemaMigrationStats.NO_MIGRATION,
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ AppSearchResult<Bundle> result = resultParcel.getResult();
+ if (result.isSuccess()) {
+ try {
+ InternalSetSchemaResponse internalSetSchemaResponse =
+ new InternalSetSchemaResponse(
+ result.getResultValue());
+ if (!internalSetSchemaResponse.isSuccess()) {
+ // check is the set schema call failed because
+ // incompatible changes. That's the only case we
+ // swallowed in the AppSearchImpl#setSchema().
+ callback.accept(AppSearchResult.newFailedResult(
+ AppSearchResult.RESULT_INVALID_SCHEMA,
+ internalSetSchemaResponse.getErrorMessage()));
+ return;
+ }
+ callback.accept(AppSearchResult.newSuccessfulResult(
+ internalSetSchemaResponse.getSetSchemaResponse()));
+ } catch (Throwable t) {
+ // TODO(b/261897334) save SDK errors/crashes and send to
+ // server for logging.
+ callback.accept(AppSearchResult.throwableToFailedResult(t));
+ }
+ } else {
+ callback.accept(AppSearchResult.newFailedResult(result));
+ }
+ });
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set schema to Icing for migration scenario.
+ *
+ * <p>First time {@link #setSchema} call with forceOverride is false gives us all incompatible
+ * changes. After trigger migrations, the second time call {@link #setSchema} will actually
+ * apply the changes.
+ */
+ private void setSchemaWithMigrations(
+ @NonNull SetSchemaRequest request,
+ @NonNull List<Bundle> schemaBundles,
+ @NonNull List<Bundle> visibilityBundles,
+ @NonNull Executor workExecutor,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
+ long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
+ long waitExecutorStartLatencyMillis = SystemClock.elapsedRealtime();
+ safeExecute(workExecutor, callback, () -> {
+ try {
+ long waitExecutorEndLatencyMillis = SystemClock.elapsedRealtime();
+ SchemaMigrationStats.Builder statsBuilder = new SchemaMigrationStats.Builder(
+ mCallerAttributionSource.getPackageName(), mDatabaseName);
+
+ // Migration process
+ // 1. Validate and retrieve all active migrators.
+ long getSchemaLatencyStartTimeMillis = SystemClock.elapsedRealtime();
+ CountDownLatch getSchemaLatch = new CountDownLatch(1);
+ AtomicReference<AppSearchResult<GetSchemaResponse>> getSchemaResultRef =
+ new AtomicReference<>();
+ getSchema(callbackExecutor, (result) -> {
+ getSchemaResultRef.set(result);
+ getSchemaLatch.countDown();
+ });
+ getSchemaLatch.await();
+ AppSearchResult<GetSchemaResponse> getSchemaResult = getSchemaResultRef.get();
+ if (!getSchemaResult.isSuccess()) {
+ // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
+ safeExecute(
+ callbackExecutor,
+ callback,
+ () -> callback.accept(
+ AppSearchResult.newFailedResult(getSchemaResult)));
+ return;
+ }
+ GetSchemaResponse getSchemaResponse =
+ Objects.requireNonNull(getSchemaResult.getResultValue());
+ int currentVersion = getSchemaResponse.getVersion();
+ int finalVersion = request.getVersion();
+ Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
+ getSchemaResponse.getSchemas(), request.getMigrators(), currentVersion,
+ finalVersion);
+ long getSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
+
+ // No need to trigger migration if no migrator is active.
+ if (activeMigrators.isEmpty()) {
+ setSchemaNoMigrations(request, schemaBundles, visibilityBundles,
+ callbackExecutor, callback);
+ return;
+ }
+
+ // 2. SetSchema with forceOverride=false, to retrieve the list of
+ // incompatible/deleted types.
+ long firstSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
+ CountDownLatch setSchemaLatch = new CountDownLatch(1);
+ AtomicReference<AppSearchResult<Bundle>> setSchemaResultRef =
+ new AtomicReference<>();
+
+ mService.setSchema(
+ mCallerAttributionSource,
+ mDatabaseName,
+ schemaBundles,
+ visibilityBundles,
+ /*forceOverride=*/ false,
+ request.getVersion(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE,
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ setSchemaResultRef.set(resultParcel.getResult());
+ setSchemaLatch.countDown();
+ }
+ });
+ setSchemaLatch.await();
+ AppSearchResult<Bundle> setSchemaResult = setSchemaResultRef.get();
+ if (!setSchemaResult.isSuccess()) {
+ // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
+ safeExecute(
+ callbackExecutor,
+ callback,
+ () -> callback.accept(
+ AppSearchResult.newFailedResult(setSchemaResult)));
+ return;
+ }
+ InternalSetSchemaResponse internalSetSchemaResponse1 =
+ new InternalSetSchemaResponse(setSchemaResult.getResultValue());
+ long firstSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
+
+ // 3. If forceOverride is false, check that all incompatible types will be migrated.
+ // If some aren't we must throw an error, rather than proceeding and deleting those
+ // types.
+ SchemaMigrationUtil.checkDeletedAndIncompatibleAfterMigration(
+ internalSetSchemaResponse1, activeMigrators.keySet());
+
+ try (AppSearchMigrationHelper migrationHelper = new AppSearchMigrationHelper(
+ mService, mUserHandle, mCallerAttributionSource, mDatabaseName,
+ request.getSchemas())) {
+
+ // 4. Trigger migration for all migrators.
+ long queryAndTransformLatencyStartTimeMillis = SystemClock.elapsedRealtime();
+ for (Map.Entry<String, Migrator> entry : activeMigrators.entrySet()) {
+ migrationHelper.queryAndTransform(/*schemaType=*/ entry.getKey(),
+ /*migrator=*/ entry.getValue(), currentVersion,
+ finalVersion, statsBuilder);
+ }
+ long queryAndTransformLatencyEndTimeMillis = SystemClock.elapsedRealtime();
+
+ // 5. SetSchema a second time with forceOverride=true if the first attempted
+ // failed.
+ long secondSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
+ InternalSetSchemaResponse internalSetSchemaResponse;
+ if (internalSetSchemaResponse1.isSuccess()) {
+ internalSetSchemaResponse = internalSetSchemaResponse1;
+ } else {
+ CountDownLatch setSchema2Latch = new CountDownLatch(1);
+ AtomicReference<AppSearchResult<Bundle>> setSchema2ResultRef =
+ new AtomicReference<>();
+ // only trigger second setSchema() call if the first one is fail.
+ mService.setSchema(
+ mCallerAttributionSource,
+ mDatabaseName,
+ schemaBundles,
+ visibilityBundles,
+ /*forceOverride=*/ true,
+ request.getVersion(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ SchemaMigrationStats.SECOND_CALL_APPLY_NEW_SCHEMA,
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ setSchema2ResultRef.set(resultParcel.getResult());
+ setSchema2Latch.countDown();
+ }
+ });
+ setSchema2Latch.await();
+ AppSearchResult<Bundle> setSchema2Result = setSchema2ResultRef.get();
+ if (!setSchema2Result.isSuccess()) {
+ // we failed to set the schema in second time with forceOverride = true,
+ // which is an impossible case. Since we only swallow the incompatible
+ // error in the first setSchema call, all other errors will be thrown at
+ // the first time.
+ // TODO(b/261897334) save SDK errors/crashes and send to server for
+ // logging.
+ safeExecute(
+ callbackExecutor,
+ callback,
+ () -> callback.accept(
+ AppSearchResult.newFailedResult(setSchema2Result)));
+ return;
+ }
+ InternalSetSchemaResponse internalSetSchemaResponse2 =
+ new InternalSetSchemaResponse(setSchema2Result.getResultValue());
+ if (!internalSetSchemaResponse2.isSuccess()) {
+ // Impossible case, we just set forceOverride to be true, we should
+ // never fail in incompatible changes. And all other cases should failed
+ // during the first call.
+ // TODO(b/261897334) save SDK errors/crashes and send to server for
+ // logging.
+ safeExecute(
+ callbackExecutor,
+ callback,
+ () -> callback.accept(
+ AppSearchResult.newFailedResult(
+ AppSearchResult.RESULT_INTERNAL_ERROR,
+ internalSetSchemaResponse2.getErrorMessage())));
+ return;
+ }
+ internalSetSchemaResponse = internalSetSchemaResponse2;
+ }
+ long secondSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
+
+ statsBuilder
+ .setExecutorAcquisitionLatencyMillis(
+ (int) (waitExecutorEndLatencyMillis
+ - waitExecutorStartLatencyMillis))
+ .setGetSchemaLatencyMillis(
+ (int)(getSchemaLatencyEndTimeMillis
+ - getSchemaLatencyStartTimeMillis))
+ .setFirstSetSchemaLatencyMillis(
+ (int)(firstSetSchemaLatencyEndTimeMillis
+ - firstSetSchemaLatencyStartMillis))
+ .setIsFirstSetSchemaSuccess(internalSetSchemaResponse1.isSuccess())
+ .setQueryAndTransformLatencyMillis(
+ (int)(queryAndTransformLatencyEndTimeMillis -
+ queryAndTransformLatencyStartTimeMillis))
+ .setSecondSetSchemaLatencyMillis(
+ (int)(secondSetSchemaLatencyEndTimeMillis
+ - secondSetSchemaLatencyStartMillis));
+ SetSchemaResponse.Builder responseBuilder = internalSetSchemaResponse
+ .getSetSchemaResponse()
+ .toBuilder()
+ .addMigratedTypes(activeMigrators.keySet());
+
+ // 6. Put all the migrated documents into the index, now that the new schema is
+ // set.
+ AppSearchResult<SetSchemaResponse> putResult =
+ migrationHelper.putMigratedDocuments(
+ responseBuilder, statsBuilder, totalLatencyStartTimeMillis);
+ safeExecute(callbackExecutor, callback, () -> callback.accept(putResult));
+ }
+ } catch (Throwable t) {
+ // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
+ safeExecute(
+ callbackExecutor,
+ callback,
+ () -> callback.accept(AppSearchResult.throwableToFailedResult(t)));
+ }
+ });
+ }
+}
diff --git a/android-34/android/app/appsearch/BatchResultCallback.java b/android-34/android/app/appsearch/BatchResultCallback.java
new file mode 100644
index 0000000..28f8a7a
--- /dev/null
+++ b/android-34/android/app/appsearch/BatchResultCallback.java
@@ -0,0 +1,58 @@
+/*
+ * 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 android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * The callback interface to return {@link AppSearchBatchResult}.
+ *
+ * @param <KeyType> The type of the keys for {@link AppSearchBatchResult#getSuccesses} and
+ * {@link AppSearchBatchResult#getFailures}.
+ * @param <ValueType> The type of result objects associated with the keys.
+ */
+public interface BatchResultCallback<KeyType, ValueType> {
+
+ /**
+ * Called when {@link AppSearchBatchResult} results are ready.
+ *
+ * @param result The result of the executed request.
+ */
+ void onResult(@NonNull AppSearchBatchResult<KeyType, ValueType> result);
+
+ /**
+ * Called when a system error occurs.
+ *
+ * <p>This method is only called the infrastructure is fundamentally broken or unavailable, such
+ * that none of the requests could be started. For example, it will be called if the AppSearch
+ * service unexpectedly fails to initialize and can't be recovered by any means, or if
+ * communicating to the server over Binder fails (e.g. system service crashed or device is
+ * rebooting).
+ *
+ * <p>The error is not expected to be recoverable and there is no specific recommended action
+ * other than displaying a permanent message to the user.
+ *
+ * <p>Normal errors that are caused by invalid inputs or recoverable/retriable situations
+ * are reported associated with the input that caused them via the {@link #onResult} method.
+ *
+ * @param throwable an exception describing the system error
+ */
+ default void onSystemError(@Nullable Throwable throwable) {
+ throw new RuntimeException("Unrecoverable system error", throwable);
+ }
+}
diff --git a/android-34/android/app/appsearch/FeatureConstants.java b/android-34/android/app/appsearch/FeatureConstants.java
new file mode 100644
index 0000000..95ad082
--- /dev/null
+++ b/android-34/android/app/appsearch/FeatureConstants.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+/**
+ * A class that encapsulates all feature constants that are accessible in AppSearch framework.
+ *
+ * <p>All fields in this class is referring in {@link Features}. If you add/remove any field in this
+ * class, you should also change {@link Features}.
+ *
+ * @see Features
+ * @hide
+ */
+public interface FeatureConstants {
+ /** Feature constants for {@link Features#NUMERIC_SEARCH}. */
+ String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+
+ /** Feature constants for {@link Features#VERBATIM_SEARCH}. */
+ String VERBATIM_SEARCH = "VERBATIM_SEARCH";
+
+ /** Feature constants for {@link Features#LIST_FILTER_QUERY_LANGUAGE}. */
+ String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+}
diff --git a/android-34/android/app/appsearch/GenericDocument.java b/android-34/android/app/appsearch/GenericDocument.java
new file mode 100644
index 0000000..71c3700
--- /dev/null
+++ b/android-34/android/app/appsearch/GenericDocument.java
@@ -0,0 +1,1398 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.appsearch.PropertyPath.PathSegment;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.util.BundleUtil;
+import android.app.appsearch.util.IndentingStringBuilder;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a document unit.
+ *
+ * <p>Documents contain structured data conforming to their {@link AppSearchSchema} type. Each
+ * document is uniquely identified by a namespace and a String ID within that namespace.
+ *
+ * <p>Documents are constructed by using the {@link GenericDocument.Builder}.
+ *
+ * @see AppSearchSession#put
+ * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#search
+ */
+public class GenericDocument {
+ private static final String TAG = "AppSearchGenericDocumen";
+
+ /** The maximum number of indexed properties a document can have. */
+ private static final int MAX_INDEXED_PROPERTIES = 16;
+
+ /** The default score of document. */
+ private static final int DEFAULT_SCORE = 0;
+
+ /** The default time-to-live in millisecond of a document, which is infinity. */
+ private static final long DEFAULT_TTL_MILLIS = 0L;
+
+ private static final String PROPERTIES_FIELD = "properties";
+ private static final String BYTE_ARRAY_FIELD = "byteArray";
+ private static final String SCHEMA_TYPE_FIELD = "schemaType";
+ private static final String ID_FIELD = "id";
+ private static final String SCORE_FIELD = "score";
+ private static final String TTL_MILLIS_FIELD = "ttlMillis";
+ private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
+ private static final String NAMESPACE_FIELD = "namespace";
+
+ /**
+ * The maximum number of indexed properties a document can have.
+ *
+ * <p>Indexed properties are properties which are strings where the {@link
+ * AppSearchSchema.StringPropertyConfig#getIndexingType} value is anything other than {@link
+ * AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE}.
+ */
+ public static int getMaxIndexedProperties() {
+ return MAX_INDEXED_PROPERTIES;
+ }
+
+ /**
+ * Contains all {@link GenericDocument} information in a packaged format.
+ *
+ * <p>Keys are the {@code *_FIELD} constants in this class.
+ */
+ @NonNull final Bundle mBundle;
+
+ /** Contains all properties in {@link GenericDocument} to support getting properties via name */
+ @NonNull private final Bundle mProperties;
+
+ @NonNull private final String mId;
+ @NonNull private final String mSchemaType;
+ private final long mCreationTimestampMillis;
+ @Nullable private Integer mHashCode;
+
+ /**
+ * Rebuilds a {@link GenericDocument} from a bundle.
+ *
+ * @param bundle Packaged {@link GenericDocument} data, such as the result of {@link
+ * #getBundle}.
+ * @hide
+ */
+ @SuppressWarnings("deprecation")
+ public GenericDocument(@NonNull Bundle bundle) {
+ Objects.requireNonNull(bundle);
+ mBundle = bundle;
+ mProperties = Objects.requireNonNull(bundle.getParcelable(PROPERTIES_FIELD));
+ mId = Objects.requireNonNull(mBundle.getString(ID_FIELD));
+ mSchemaType = Objects.requireNonNull(mBundle.getString(SCHEMA_TYPE_FIELD));
+ mCreationTimestampMillis =
+ mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD, System.currentTimeMillis());
+ }
+
+ /**
+ * Creates a new {@link GenericDocument} from an existing instance.
+ *
+ * <p>This method should be only used by constructor of a subclass.
+ */
+ protected GenericDocument(@NonNull GenericDocument document) {
+ this(document.mBundle);
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /** Returns the unique identifier of the {@link GenericDocument}. */
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ /** Returns the namespace of the {@link GenericDocument}. */
+ @NonNull
+ public String getNamespace() {
+ return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/ "");
+ }
+
+ /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
+ @NonNull
+ public String getSchemaType() {
+ return mSchemaType;
+ }
+
+ /**
+ * Returns the creation timestamp of the {@link GenericDocument}, in milliseconds.
+ *
+ * <p>The value is in the {@link System#currentTimeMillis} time base.
+ */
+ @CurrentTimeMillisLong
+ public long getCreationTimestampMillis() {
+ return mCreationTimestampMillis;
+ }
+
+ /**
+ * Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
+ *
+ * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
+ * {@code creationTimestampMillis + ttlMillis}, measured in the {@link System#currentTimeMillis}
+ * time base, the document will be auto-deleted.
+ *
+ * <p>The default value is 0, which means the document is permanent and won't be auto-deleted
+ * until the app is uninstalled or {@link AppSearchSession#remove} is called.
+ */
+ public long getTtlMillis() {
+ return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
+ }
+
+ /**
+ * Returns the score of the {@link GenericDocument}.
+ *
+ * <p>The score is a query-independent measure of the document's quality, relative to other
+ * {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
+ *
+ * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
+ * Documents with higher scores are considered better than documents with lower scores.
+ *
+ * <p>Any non-negative integer can be used a score.
+ */
+ public int getScore() {
+ return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE);
+ }
+
+ /** Returns the names of all properties defined in this document. */
+ @NonNull
+ public Set<String> getPropertyNames() {
+ return Collections.unmodifiableSet(mProperties.keySet());
+ }
+
+ /**
+ * Retrieves the property value with the given path as {@link Object}.
+ *
+ * <p>A path can be a simple property name, such as those returned by {@link #getPropertyNames}.
+ * It may also be a dot-delimited path through the nested document hierarchy, with nested {@link
+ * GenericDocument} properties accessed via {@code '.'} and repeated properties optionally
+ * indexed into via {@code [n]}.
+ *
+ * <p>For example, given the following {@link GenericDocument}:
+ *
+ * <pre>
+ * (Message) {
+ * from: "sender@example.com"
+ * to: [{
+ * name: "Albert Einstein"
+ * email: "einstein@example.com"
+ * }, {
+ * name: "Marie Curie"
+ * email: "curie@example.com"
+ * }]
+ * tags: ["important", "inbox"]
+ * subject: "Hello"
+ * }
+ * </pre>
+ *
+ * <p>Here are some example paths and their results:
+ *
+ * <ul>
+ * <li>{@code "from"} returns {@code "sender@example.com"} as a {@link String} array with one
+ * element
+ * <li>{@code "to"} returns the two nested documents containing contact information as a
+ * {@link GenericDocument} array with two elements
+ * <li>{@code "to[1]"} returns the second nested document containing Marie Curie's contact
+ * information as a {@link GenericDocument} array with one element
+ * <li>{@code "to[1].email"} returns {@code "curie@example.com"}
+ * <li>{@code "to[100].email"} returns {@code null} as this particular document does not have
+ * that many elements in its {@code "to"} array.
+ * <li>{@code "to.email"} aggregates emails across all nested documents that have them,
+ * returning {@code ["einstein@example.com", "curie@example.com"]} as a {@link String}
+ * array with two elements.
+ * </ul>
+ *
+ * <p>If you know the expected type of the property you are retrieving, it is recommended to use
+ * one of the typed versions of this method instead, such as {@link #getPropertyString} or
+ * {@link #getPropertyStringArray}.
+ *
+ * <p>If the property was assigned as an empty array using one of the {@code
+ * Builder#setProperty} functions, this method will return an empty array. If no such property
+ * exists at all, this method returns {@code null}.
+ *
+ * <p>Note: If the property is an empty {@link GenericDocument}[] or {@code byte[][]}, this
+ * method will return a {@code null} value in versions of Android prior to {@link
+ * android.os.Build.VERSION_CODES#TIRAMISU Android T}. Starting in Android T it will return an
+ * empty array if the property has been set as an empty array, matching the behavior of other
+ * property types.
+ *
+ * @param path The path to look for.
+ * @return The entry with the given path as an object or {@code null} if there is no such path.
+ * The returned object will be one of the following types: {@code String[]}, {@code long[]},
+ * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.
+ */
+ @Nullable
+ public Object getProperty(@NonNull String path) {
+ Objects.requireNonNull(path);
+ Object rawValue =
+ getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/ 0, mBundle);
+
+ // Unpack the raw value into the types the user expects, if required.
+ if (rawValue instanceof Bundle) {
+ // getRawPropertyFromRawDocument may return a document as a bare Bundle as a performance
+ // optimization for lookups.
+ GenericDocument document = new GenericDocument((Bundle) rawValue);
+ return new GenericDocument[] {document};
+ }
+
+ if (rawValue instanceof List) {
+ // byte[][] fields are packed into List<Bundle> where each Bundle contains just a single
+ // entry: BYTE_ARRAY_FIELD -> byte[].
+ @SuppressWarnings("unchecked")
+ List<Bundle> bundles = (List<Bundle>) rawValue;
+ byte[][] bytes = new byte[bundles.size()][];
+ for (int i = 0; i < bundles.size(); i++) {
+ Bundle bundle = bundles.get(i);
+ if (bundle == null) {
+ Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);
+ continue;
+ }
+ byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD);
+ if (innerBytes == null) {
+ Log.e(TAG, "The bundle at " + i + " contains a null byte[].");
+ continue;
+ }
+ bytes[i] = innerBytes;
+ }
+ return bytes;
+ }
+
+ if (rawValue instanceof Parcelable[]) {
+ // The underlying Bundle of nested GenericDocuments is packed into a Parcelable array.
+ // We must unpack it into GenericDocument instances.
+ Parcelable[] bundles = (Parcelable[]) rawValue;
+ GenericDocument[] documents = new GenericDocument[bundles.length];
+ for (int i = 0; i < bundles.length; i++) {
+ if (bundles[i] == null) {
+ Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);
+ continue;
+ }
+ if (!(bundles[i] instanceof Bundle)) {
+ Log.e(
+ TAG,
+ "The inner element at "
+ + i
+ + " is a "
+ + bundles[i].getClass()
+ + ", not a Bundle for path: "
+ + path);
+ continue;
+ }
+ documents[i] = new GenericDocument((Bundle) bundles[i]);
+ }
+ return documents;
+ }
+
+ // Otherwise the raw property is the same as the final property and needs no transformation.
+ return rawValue;
+ }
+
+ /**
+ * Looks up a property path within the given document bundle.
+ *
+ * <p>The return value may be any of GenericDocument's internal repeated storage types
+ * (String[], long[], double[], boolean[], ArrayList<Bundle>, Parcelable[]).
+ *
+ * <p>Usually, this method takes a path and loops over it to get a property from the bundle. But
+ * in the case where we collect documents across repeated nested documents, we need to recurse
+ * back into this method, and so we also keep track of the index into the path.
+ *
+ * @param path the PropertyPath object representing the path
+ * @param pathIndex the index into the path we start at
+ * @param documentBundle the bundle that contains the path we are looking up
+ * @return the raw property
+ */
+ @Nullable
+ @SuppressWarnings("deprecation")
+ private static Object getRawPropertyFromRawDocument(
+ @NonNull PropertyPath path, int pathIndex, @NonNull Bundle documentBundle) {
+ Objects.requireNonNull(path);
+ Objects.requireNonNull(documentBundle);
+ Bundle properties = Objects.requireNonNull(documentBundle.getBundle(PROPERTIES_FIELD));
+
+ for (int i = pathIndex; i < path.size(); i++) {
+ PathSegment segment = path.get(i);
+
+ Object currentElementValue = properties.get(segment.getPropertyName());
+
+ if (currentElementValue == null) {
+ return null;
+ }
+
+ // If the current PathSegment has an index, we now need to update currentElementValue to
+ // contain the value of the indexed property. For example, for a path segment like
+ // "recipients[0]", currentElementValue now contains the value of "recipients" while we
+ // need the value of "recipients[0]".
+ int index = segment.getPropertyIndex();
+ if (index != PathSegment.NON_REPEATED_CARDINALITY) {
+ // Extract the right array element
+ Object extractedValue = null;
+ if (currentElementValue instanceof String[]) {
+ String[] stringValues = (String[]) currentElementValue;
+ if (index < stringValues.length) {
+ extractedValue = Arrays.copyOfRange(stringValues, index, index + 1);
+ }
+ } else if (currentElementValue instanceof long[]) {
+ long[] longValues = (long[]) currentElementValue;
+ if (index < longValues.length) {
+ extractedValue = Arrays.copyOfRange(longValues, index, index + 1);
+ }
+ } else if (currentElementValue instanceof double[]) {
+ double[] doubleValues = (double[]) currentElementValue;
+ if (index < doubleValues.length) {
+ extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1);
+ }
+ } else if (currentElementValue instanceof boolean[]) {
+ boolean[] booleanValues = (boolean[]) currentElementValue;
+ if (index < booleanValues.length) {
+ extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1);
+ }
+ } else if (currentElementValue instanceof List) {
+ @SuppressWarnings("unchecked")
+ List<Bundle> bundles = (List<Bundle>) currentElementValue;
+ if (index < bundles.size()) {
+ extractedValue = bundles.subList(index, index + 1);
+ }
+ } else if (currentElementValue instanceof Parcelable[]) {
+ // Special optimization: to avoid creating new singleton arrays for traversing
+ // paths we return the bare document Bundle in this particular case.
+ Parcelable[] bundles = (Parcelable[]) currentElementValue;
+ if (index < bundles.length) {
+ extractedValue = bundles[index];
+ }
+ } else {
+ throw new IllegalStateException(
+ "Unsupported value type: " + currentElementValue);
+ }
+ currentElementValue = extractedValue;
+ }
+
+ // at the end of the path, either something like "...foo" or "...foo[1]"
+ if (currentElementValue == null || i == path.size() - 1) {
+ return currentElementValue;
+ }
+
+ // currentElementValue is now a Bundle or Parcelable[], we can continue down the path
+ if (currentElementValue instanceof Bundle) {
+ properties = ((Bundle) currentElementValue).getBundle(PROPERTIES_FIELD);
+ } else if (currentElementValue instanceof Parcelable[]) {
+ Parcelable[] parcelables = (Parcelable[]) currentElementValue;
+ if (parcelables.length == 1) {
+ properties = ((Bundle) parcelables[0]).getBundle(PROPERTIES_FIELD);
+ continue;
+ }
+
+ // Slowest path: we're collecting values across repeated nested docs. (Example:
+ // given a path like recipient.name, where recipient is a repeated field, we return
+ // a string array where each recipient's name is an array element).
+ //
+ // Performance note: Suppose that we have a property path "a.b.c" where the "a"
+ // property has N document values and each containing a "b" property with M document
+ // values and each of those containing a "c" property with an int array.
+ //
+ // We'll allocate a new ArrayList for each of the "b" properties, add the M int
+ // arrays from the "c" properties to it and then we'll allocate an int array in
+ // flattenAccumulator before returning that (1 + M allocation per "b" property).
+ //
+ // When we're on the "a" properties, we'll allocate an ArrayList and add the N
+ // flattened int arrays returned from the "b" properties to the list. Then we'll
+ // allocate an int array in flattenAccumulator (1 + N ("b" allocs) allocations per
+ // "a"). // So this implementation could incur 1 + N + NM allocs.
+ //
+ // However, we expect the vast majority of getProperty calls to be either for direct
+ // property names (not paths) or else property paths returned from snippetting,
+ // which always refer to exactly one property value and don't aggregate across
+ // repeated values. The implementation is optimized for these two cases, requiring
+ // no additional allocations. So we've decided that the above performance
+ // characteristics are OK for the less used path.
+ List<Object> accumulator = new ArrayList<>(parcelables.length);
+ for (Parcelable parcelable : parcelables) {
+ // recurse as we need to branch
+ Object value =
+ getRawPropertyFromRawDocument(
+ path, /*pathIndex=*/ i + 1, (Bundle) parcelable);
+ if (value != null) {
+ accumulator.add(value);
+ }
+ }
+ // Break the path traversing loop
+ return flattenAccumulator(accumulator);
+ } else {
+ Log.e(TAG, "Failed to apply path to document; no nested value found: " + path);
+ return null;
+ }
+ }
+ // Only way to get here is with an empty path list
+ return null;
+ }
+
+ /**
+ * Combines accumulated repeated properties from multiple documents into a single array.
+ *
+ * @param accumulator List containing objects of the following types: {@code String[]}, {@code
+ * long[]}, {@code double[]}, {@code boolean[]}, {@code List<Bundle>}, or {@code
+ * Parcelable[]}.
+ * @return The result of concatenating each individual list element into a larger array/list of
+ * the same type.
+ */
+ @Nullable
+ private static Object flattenAccumulator(@NonNull List<Object> accumulator) {
+ if (accumulator.isEmpty()) {
+ return null;
+ }
+ Object first = accumulator.get(0);
+ if (first instanceof String[]) {
+ int length = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ length += ((String[]) accumulator.get(i)).length;
+ }
+ String[] result = new String[length];
+ int total = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ String[] castValue = (String[]) accumulator.get(i);
+ System.arraycopy(castValue, 0, result, total, castValue.length);
+ total += castValue.length;
+ }
+ return result;
+ }
+ if (first instanceof long[]) {
+ int length = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ length += ((long[]) accumulator.get(i)).length;
+ }
+ long[] result = new long[length];
+ int total = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ long[] castValue = (long[]) accumulator.get(i);
+ System.arraycopy(castValue, 0, result, total, castValue.length);
+ total += castValue.length;
+ }
+ return result;
+ }
+ if (first instanceof double[]) {
+ int length = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ length += ((double[]) accumulator.get(i)).length;
+ }
+ double[] result = new double[length];
+ int total = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ double[] castValue = (double[]) accumulator.get(i);
+ System.arraycopy(castValue, 0, result, total, castValue.length);
+ total += castValue.length;
+ }
+ return result;
+ }
+ if (first instanceof boolean[]) {
+ int length = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ length += ((boolean[]) accumulator.get(i)).length;
+ }
+ boolean[] result = new boolean[length];
+ int total = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ boolean[] castValue = (boolean[]) accumulator.get(i);
+ System.arraycopy(castValue, 0, result, total, castValue.length);
+ total += castValue.length;
+ }
+ return result;
+ }
+ if (first instanceof List) {
+ int length = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ length += ((List<?>) accumulator.get(i)).size();
+ }
+ List<Bundle> result = new ArrayList<>(length);
+ for (int i = 0; i < accumulator.size(); i++) {
+ @SuppressWarnings("unchecked")
+ List<Bundle> castValue = (List<Bundle>) accumulator.get(i);
+ result.addAll(castValue);
+ }
+ return result;
+ }
+ if (first instanceof Parcelable[]) {
+ int length = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ length += ((Parcelable[]) accumulator.get(i)).length;
+ }
+ Parcelable[] result = new Parcelable[length];
+ int total = 0;
+ for (int i = 0; i < accumulator.size(); i++) {
+ Parcelable[] castValue = (Parcelable[]) accumulator.get(i);
+ System.arraycopy(castValue, 0, result, total, castValue.length);
+ total += castValue.length;
+ }
+ return result;
+ }
+ throw new IllegalStateException("Unexpected property type: " + first);
+ }
+
+ /**
+ * Retrieves a {@link String} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * @param path The path to look for.
+ * @return The first {@link String} associated with the given path or {@code null} if there is
+ * no such value or the value is of a different type.
+ */
+ @Nullable
+ public String getPropertyString(@NonNull String path) {
+ Objects.requireNonNull(path);
+ String[] propertyArray = getPropertyStringArray(path);
+ if (propertyArray == null || propertyArray.length == 0) {
+ return null;
+ }
+ warnIfSinglePropertyTooLong("String", path, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieves a {@code long} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * @param path The path to look for.
+ * @return The first {@code long} associated with the given path or default value {@code 0} if
+ * there is no such value or the value is of a different type.
+ */
+ public long getPropertyLong(@NonNull String path) {
+ Objects.requireNonNull(path);
+ long[] propertyArray = getPropertyLongArray(path);
+ if (propertyArray == null || propertyArray.length == 0) {
+ return 0;
+ }
+ warnIfSinglePropertyTooLong("Long", path, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieves a {@code double} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * @param path The path to look for.
+ * @return The first {@code double} associated with the given path or default value {@code 0.0}
+ * if there is no such value or the value is of a different type.
+ */
+ public double getPropertyDouble(@NonNull String path) {
+ Objects.requireNonNull(path);
+ double[] propertyArray = getPropertyDoubleArray(path);
+ if (propertyArray == null || propertyArray.length == 0) {
+ return 0.0;
+ }
+ warnIfSinglePropertyTooLong("Double", path, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieves a {@code boolean} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * @param path The path to look for.
+ * @return The first {@code boolean} associated with the given path or default value {@code
+ * false} if there is no such value or the value is of a different type.
+ */
+ public boolean getPropertyBoolean(@NonNull String path) {
+ Objects.requireNonNull(path);
+ boolean[] propertyArray = getPropertyBooleanArray(path);
+ if (propertyArray == null || propertyArray.length == 0) {
+ return false;
+ }
+ warnIfSinglePropertyTooLong("Boolean", path, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieves a {@code byte[]} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * @param path The path to look for.
+ * @return The first {@code byte[]} associated with the given path or {@code null} if there is
+ * no such value or the value is of a different type.
+ */
+ @Nullable
+ public byte[] getPropertyBytes(@NonNull String path) {
+ Objects.requireNonNull(path);
+ byte[][] propertyArray = getPropertyBytesArray(path);
+ if (propertyArray == null || propertyArray.length == 0) {
+ return null;
+ }
+ warnIfSinglePropertyTooLong("ByteArray", path, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /**
+ * Retrieves a {@link GenericDocument} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * @param path The path to look for.
+ * @return The first {@link GenericDocument} associated with the given path or {@code null} if
+ * there is no such value or the value is of a different type.
+ */
+ @Nullable
+ public GenericDocument getPropertyDocument(@NonNull String path) {
+ Objects.requireNonNull(path);
+ GenericDocument[] propertyArray = getPropertyDocumentArray(path);
+ if (propertyArray == null || propertyArray.length == 0) {
+ return null;
+ }
+ warnIfSinglePropertyTooLong("Document", path, propertyArray.length);
+ return propertyArray[0];
+ }
+
+ /** Prints a warning to logcat if the given propertyLength is greater than 1. */
+ private static void warnIfSinglePropertyTooLong(
+ @NonNull String propertyType, @NonNull String path, int propertyLength) {
+ if (propertyLength > 1) {
+ Log.w(
+ TAG,
+ "The value for \""
+ + path
+ + "\" contains "
+ + propertyLength
+ + " elements. Only the first one will be returned from "
+ + "getProperty"
+ + propertyType
+ + "(). Try getProperty"
+ + propertyType
+ + "Array().");
+ }
+ }
+
+ /**
+ * Retrieves a repeated {@code String} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * <p>If the property has not been set via {@link Builder#setPropertyString}, this method
+ * returns {@code null}.
+ *
+ * <p>If it has been set via {@link Builder#setPropertyString} to an empty {@code String[]},
+ * this method returns an empty {@code String[]}.
+ *
+ * @param path The path to look for.
+ * @return The {@code String[]} associated with the given path, or {@code null} if no value is
+ * set or the value is of a different type.
+ */
+ @Nullable
+ public String[] getPropertyStringArray(@NonNull String path) {
+ Objects.requireNonNull(path);
+ Object value = getProperty(path);
+ return safeCastProperty(path, value, String[].class);
+ }
+
+ /**
+ * Retrieves a repeated {@code long[]} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * <p>If the property has not been set via {@link Builder#setPropertyLong}, this method returns
+ * {@code null}.
+ *
+ * <p>If it has been set via {@link Builder#setPropertyLong} to an empty {@code long[]}, this
+ * method returns an empty {@code long[]}.
+ *
+ * @param path The path to look for.
+ * @return The {@code long[]} associated with the given path, or {@code null} if no value is set
+ * or the value is of a different type.
+ */
+ @Nullable
+ public long[] getPropertyLongArray(@NonNull String path) {
+ Objects.requireNonNull(path);
+ Object value = getProperty(path);
+ return safeCastProperty(path, value, long[].class);
+ }
+
+ /**
+ * Retrieves a repeated {@code double} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * <p>If the property has not been set via {@link Builder#setPropertyDouble}, this method
+ * returns {@code null}.
+ *
+ * <p>If it has been set via {@link Builder#setPropertyDouble} to an empty {@code double[]},
+ * this method returns an empty {@code double[]}.
+ *
+ * @param path The path to look for.
+ * @return The {@code double[]} associated with the given path, or {@code null} if no value is
+ * set or the value is of a different type.
+ */
+ @Nullable
+ public double[] getPropertyDoubleArray(@NonNull String path) {
+ Objects.requireNonNull(path);
+ Object value = getProperty(path);
+ return safeCastProperty(path, value, double[].class);
+ }
+
+ /**
+ * Retrieves a repeated {@code boolean} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * <p>If the property has not been set via {@link Builder#setPropertyBoolean}, this method
+ * returns {@code null}.
+ *
+ * <p>If it has been set via {@link Builder#setPropertyBoolean} to an empty {@code boolean[]},
+ * this method returns an empty {@code boolean[]}.
+ *
+ * @param path The path to look for.
+ * @return The {@code boolean[]} associated with the given path, or {@code null} if no value is
+ * set or the value is of a different type.
+ */
+ @Nullable
+ public boolean[] getPropertyBooleanArray(@NonNull String path) {
+ Objects.requireNonNull(path);
+ Object value = getProperty(path);
+ return safeCastProperty(path, value, boolean[].class);
+ }
+
+ /**
+ * Retrieves a {@code byte[][]} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * <p>If the property has not been set via {@link Builder#setPropertyBytes}, this method returns
+ * {@code null}.
+ *
+ * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]}, this
+ * method returns an empty {@code byte[][]} starting in {@link
+ * android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier versions of
+ * Android.
+ *
+ * @param path The path to look for.
+ * @return The {@code byte[][]} associated with the given path, or {@code null} if no value is
+ * set or the value is of a different type.
+ */
+ @SuppressLint("ArrayReturn")
+ @Nullable
+ public byte[][] getPropertyBytesArray(@NonNull String path) {
+ Objects.requireNonNull(path);
+ Object value = getProperty(path);
+ return safeCastProperty(path, value, byte[][].class);
+ }
+
+ /**
+ * Retrieves a repeated {@link GenericDocument} property by path.
+ *
+ * <p>See {@link #getProperty} for a detailed description of the path syntax.
+ *
+ * <p>If the property has not been set via {@link Builder#setPropertyDocument}, this method
+ * returns {@code null}.
+ *
+ * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty {@code
+ * GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting in {@link
+ * android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier versions of
+ * Android.
+ *
+ * @param path The path to look for.
+ * @return The {@link GenericDocument}[] associated with the given path, or {@code null} if no
+ * value is set or the value is of a different type.
+ */
+ @SuppressLint("ArrayReturn")
+ @Nullable
+ public GenericDocument[] getPropertyDocumentArray(@NonNull String path) {
+ Objects.requireNonNull(path);
+ Object value = getProperty(path);
+ return safeCastProperty(path, value, GenericDocument[].class);
+ }
+
+ /**
+ * Casts a repeated property to the provided type, logging an error and returning {@code null}
+ * if the cast fails.
+ *
+ * @param path Path to the property within the document. Used for logging.
+ * @param value Value of the property
+ * @param tClass Class to cast the value into
+ */
+ @Nullable
+ private static <T> T safeCastProperty(
+ @NonNull String path, @Nullable Object value, @NonNull Class<T> tClass) {
+ if (value == null) {
+ return null;
+ }
+ try {
+ return tClass.cast(value);
+ } catch (ClassCastException e) {
+ Log.w(TAG, "Error casting to requested type for path \"" + path + "\"", e);
+ return null;
+ }
+ }
+
+ /**
+ * Copies the contents of this {@link GenericDocument} into a new {@link
+ * GenericDocument.Builder}.
+ *
+ * <p>The returned builder is a deep copy whose data is separate from this document.
+ *
+ * @hide
+ */
+ // TODO(b/171882200): Expose this API in Android T
+ @NonNull
+ public GenericDocument.Builder<GenericDocument.Builder<?>> toBuilder() {
+ Bundle clonedBundle = BundleUtil.deepCopy(mBundle);
+ return new GenericDocument.Builder<>(clonedBundle);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof GenericDocument)) {
+ return false;
+ }
+ GenericDocument otherDocument = (GenericDocument) other;
+ return BundleUtil.deepEquals(this.mBundle, otherDocument.mBundle);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode = BundleUtil.deepHashCode(mBundle);
+ }
+ return mHashCode;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+ appendGenericDocumentString(stringBuilder);
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Appends a debug string for the {@link GenericDocument} instance to the given string builder.
+ *
+ * @param builder the builder to append to.
+ */
+ void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) {
+ Objects.requireNonNull(builder);
+
+ builder.append("{\n");
+ builder.increaseIndentLevel();
+
+ builder.append("namespace: \"").append(getNamespace()).append("\",\n");
+ builder.append("id: \"").append(getId()).append("\",\n");
+ builder.append("score: ").append(getScore()).append(",\n");
+ builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+ builder.append("creationTimestampMillis: ")
+ .append(getCreationTimestampMillis())
+ .append(",\n");
+ builder.append("timeToLiveMillis: ").append(getTtlMillis()).append(",\n");
+
+ builder.append("properties: {\n");
+
+ String[] sortedProperties = getPropertyNames().toArray(new String[0]);
+ Arrays.sort(sortedProperties);
+
+ for (int i = 0; i < sortedProperties.length; i++) {
+ Object property = Objects.requireNonNull(getProperty(sortedProperties[i]));
+ builder.increaseIndentLevel();
+ appendPropertyString(sortedProperties[i], property, builder);
+ if (i != sortedProperties.length - 1) {
+ builder.append(",\n");
+ }
+ builder.decreaseIndentLevel();
+ }
+
+ builder.append("\n");
+ builder.append("}");
+
+ builder.decreaseIndentLevel();
+ builder.append("\n");
+ builder.append("}");
+ }
+
+ /**
+ * Appends a debug string for the given document property to the given string builder.
+ *
+ * @param propertyName name of property to create string for.
+ * @param property property object to create string for.
+ * @param builder the builder to append to.
+ */
+ private void appendPropertyString(
+ @NonNull String propertyName,
+ @NonNull Object property,
+ @NonNull IndentingStringBuilder builder) {
+ Objects.requireNonNull(propertyName);
+ Objects.requireNonNull(property);
+ Objects.requireNonNull(builder);
+
+ builder.append("\"").append(propertyName).append("\": [");
+ if (property instanceof GenericDocument[]) {
+ GenericDocument[] documentValues = (GenericDocument[]) property;
+ for (int i = 0; i < documentValues.length; ++i) {
+ builder.append("\n");
+ builder.increaseIndentLevel();
+ documentValues[i].appendGenericDocumentString(builder);
+ if (i != documentValues.length - 1) {
+ builder.append(",");
+ }
+ builder.append("\n");
+ builder.decreaseIndentLevel();
+ }
+ builder.append("]");
+ } else {
+ int propertyArrLength = Array.getLength(property);
+ for (int i = 0; i < propertyArrLength; i++) {
+ Object propertyElement = Array.get(property, i);
+ if (propertyElement instanceof String) {
+ builder.append("\"").append((String) propertyElement).append("\"");
+ } else if (propertyElement instanceof byte[]) {
+ builder.append(Arrays.toString((byte[]) propertyElement));
+ } else {
+ builder.append(propertyElement.toString());
+ }
+ if (i != propertyArrLength - 1) {
+ builder.append(", ");
+ } else {
+ builder.append("]");
+ }
+ }
+ }
+ }
+
+ /**
+ * The builder class for {@link GenericDocument}.
+ *
+ * @param <BuilderType> Type of subclass who extends this.
+ */
+ // This builder is specifically designed to be extended by classes deriving from
+ // GenericDocument.
+ @SuppressLint("StaticFinalBuilder")
+ public static class Builder<BuilderType extends Builder> {
+ private Bundle mBundle;
+ private Bundle mProperties;
+ private final BuilderType mBuilderTypeInstance;
+ private boolean mBuilt = false;
+
+ /**
+ * Creates a new {@link GenericDocument.Builder}.
+ *
+ * <p>Document IDs are unique within a namespace.
+ *
+ * <p>The number of namespaces per app should be kept small for efficiency reasons.
+ *
+ * @param namespace the namespace to set for the {@link GenericDocument}.
+ * @param id the unique identifier for the {@link GenericDocument} in its namespace.
+ * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+ * provided {@code schemaType} must be defined using {@link AppSearchSession#setSchema}
+ * prior to inserting a document of this {@code schemaType} into the AppSearch index
+ * using {@link AppSearchSession#put}. Otherwise, the document will be rejected by
+ * {@link AppSearchSession#put} with result code {@link
+ * AppSearchResult#RESULT_NOT_FOUND}.
+ */
+ @SuppressWarnings("unchecked")
+ public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+ Objects.requireNonNull(namespace);
+ Objects.requireNonNull(id);
+ Objects.requireNonNull(schemaType);
+
+ mBundle = new Bundle();
+ mBuilderTypeInstance = (BuilderType) this;
+ mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
+ mBundle.putString(GenericDocument.ID_FIELD, id);
+ mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
+ mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
+ mBundle.putInt(GenericDocument.SCORE_FIELD, DEFAULT_SCORE);
+
+ mProperties = new Bundle();
+ mBundle.putBundle(PROPERTIES_FIELD, mProperties);
+ }
+
+ /**
+ * Creates a new {@link GenericDocument.Builder} from the given Bundle.
+ *
+ * <p>The bundle is NOT copied.
+ */
+ @SuppressWarnings("unchecked")
+ Builder(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ // mProperties is NonNull and initialized to empty Bundle() in builder.
+ mProperties = Objects.requireNonNull(mBundle.getBundle(PROPERTIES_FIELD));
+ mBuilderTypeInstance = (BuilderType) this;
+ }
+
+ /**
+ * Sets the app-defined namespace this document resides in, changing the value provided in
+ * the constructor. No special values are reserved or understood by the infrastructure.
+ *
+ * <p>Document IDs are unique within a namespace.
+ *
+ * <p>The number of namespaces per app should be kept small for efficiency reasons.
+ *
+ * @hide
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setNamespace(@NonNull String namespace) {
+ Objects.requireNonNull(namespace);
+ resetIfBuilt();
+ mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets the ID of this document, changing the value provided in the constructor. No special
+ * values are reserved or understood by the infrastructure.
+ *
+ * <p>Document IDs are unique within a namespace.
+ *
+ * @hide
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setId(@NonNull String id) {
+ Objects.requireNonNull(id);
+ resetIfBuilt();
+ mBundle.putString(GenericDocument.ID_FIELD, id);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets the schema type of this document, changing the value provided in the constructor.
+ *
+ * <p>To successfully index a document, the schema type must match the name of an {@link
+ * AppSearchSchema} object previously provided to {@link AppSearchSession#setSchema}.
+ *
+ * @hide
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setSchemaType(@NonNull String schemaType) {
+ Objects.requireNonNull(schemaType);
+ resetIfBuilt();
+ mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets the score of the {@link GenericDocument}.
+ *
+ * <p>The score is a query-independent measure of the document's quality, relative to other
+ * {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
+ *
+ * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
+ * Documents with higher scores are considered better than documents with lower scores.
+ *
+ * <p>Any non-negative integer can be used a score. By default, scores are set to 0.
+ *
+ * @param score any non-negative {@code int} representing the document's score.
+ * @throws IllegalArgumentException if the score is negative.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
+ if (score < 0) {
+ throw new IllegalArgumentException("Document score cannot be negative.");
+ }
+ resetIfBuilt();
+ mBundle.putInt(GenericDocument.SCORE_FIELD, score);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds.
+ *
+ * <p>This should be set using a value obtained from the {@link System#currentTimeMillis}
+ * time base.
+ *
+ * <p>If this method is not called, this will be set to the time the object is built.
+ *
+ * @param creationTimestampMillis a creation timestamp in milliseconds.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setCreationTimestampMillis(
+ @CurrentTimeMillisLong long creationTimestampMillis) {
+ resetIfBuilt();
+ mBundle.putLong(
+ GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, creationTimestampMillis);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
+ *
+ * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
+ * {@code creationTimestampMillis + ttlMillis}, measured in the {@link
+ * System#currentTimeMillis} time base, the document will be auto-deleted.
+ *
+ * <p>The default value is 0, which means the document is permanent and won't be
+ * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is called.
+ *
+ * @param ttlMillis a non-negative duration in milliseconds.
+ * @throws IllegalArgumentException if ttlMillis is negative.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setTtlMillis(long ttlMillis) {
+ if (ttlMillis < 0) {
+ throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
+ }
+ resetIfBuilt();
+ mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code String} values for a property, replacing its previous values.
+ *
+ * @param name the name associated with the {@code values}. Must match the name for this
+ * property as given in {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param values the {@code String} values of the property.
+ * @throws IllegalArgumentException if no values are provided, or if a passed in {@code
+ * String} is {@code null} or "".
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(values);
+ resetIfBuilt();
+ putInPropertyBundle(name, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code boolean} values for a property, replacing its previous
+ * values.
+ *
+ * @param name the name associated with the {@code values}. Must match the name for this
+ * property as given in {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param values the {@code boolean} values of the property.
+ * @throws IllegalArgumentException if the name is empty or {@code null}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(values);
+ resetIfBuilt();
+ putInPropertyBundle(name, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code long} values for a property, replacing its previous values.
+ *
+ * @param name the name associated with the {@code values}. Must match the name for this
+ * property as given in {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param values the {@code long} values of the property.
+ * @throws IllegalArgumentException if the name is empty or {@code null}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(values);
+ resetIfBuilt();
+ putInPropertyBundle(name, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code double} values for a property, replacing its previous values.
+ *
+ * @param name the name associated with the {@code values}. Must match the name for this
+ * property as given in {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param values the {@code double} values of the property.
+ * @throws IllegalArgumentException if the name is empty or {@code null}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(values);
+ resetIfBuilt();
+ putInPropertyBundle(name, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@code byte[]} for a property, replacing its previous values.
+ *
+ * @param name the name associated with the {@code values}. Must match the name for this
+ * property as given in {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param values the {@code byte[]} of the property.
+ * @throws IllegalArgumentException if no values are provided, or if a passed in {@code
+ * byte[]} is {@code null}, or if name is empty.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(values);
+ resetIfBuilt();
+ putInPropertyBundle(name, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Sets one or multiple {@link GenericDocument} values for a property, replacing its
+ * previous values.
+ *
+ * @param name the name associated with the {@code values}. Must match the name for this
+ * property as given in {@link AppSearchSchema.PropertyConfig#getName}.
+ * @param values the {@link GenericDocument} values of the property.
+ * @throws IllegalArgumentException if no values are provided, or if a passed in {@link
+ * GenericDocument} is {@code null}, or if name is empty.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType setPropertyDocument(
+ @NonNull String name, @NonNull GenericDocument... values) {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(values);
+ resetIfBuilt();
+ putInPropertyBundle(name, values);
+ return mBuilderTypeInstance;
+ }
+
+ /**
+ * Clears the value for the property with the given name.
+ *
+ * <p>Note that this method does not support property paths.
+ *
+ * @param name The name of the property to clear.
+ * @hide
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public BuilderType clearProperty(@NonNull String name) {
+ Objects.requireNonNull(name);
+ resetIfBuilt();
+ mProperties.remove(name);
+ return mBuilderTypeInstance;
+ }
+
+ private void putInPropertyBundle(@NonNull String name, @NonNull String[] values)
+ throws IllegalArgumentException {
+ validatePropertyName(name);
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("The String at " + i + " is null.");
+ }
+ }
+ mProperties.putStringArray(name, values);
+ }
+
+ private void putInPropertyBundle(@NonNull String name, @NonNull boolean[] values) {
+ validatePropertyName(name);
+ mProperties.putBooleanArray(name, values);
+ }
+
+ private void putInPropertyBundle(@NonNull String name, @NonNull double[] values) {
+ validatePropertyName(name);
+ mProperties.putDoubleArray(name, values);
+ }
+
+ private void putInPropertyBundle(@NonNull String name, @NonNull long[] values) {
+ validatePropertyName(name);
+ mProperties.putLongArray(name, values);
+ }
+
+ /**
+ * Converts and saves a byte[][] into {@link #mProperties}.
+ *
+ * <p>Bundle doesn't support for two dimension array byte[][], we are converting byte[][]
+ * into ArrayList<Bundle>, and each elements will contain a one dimension byte[].
+ */
+ private void putInPropertyBundle(@NonNull String name, @NonNull byte[][] values) {
+ validatePropertyName(name);
+ ArrayList<Bundle> bundles = new ArrayList<>(values.length);
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("The byte[] at " + i + " is null.");
+ }
+ Bundle bundle = new Bundle();
+ bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]);
+ bundles.add(bundle);
+ }
+ mProperties.putParcelableArrayList(name, bundles);
+ }
+
+ private void putInPropertyBundle(@NonNull String name, @NonNull GenericDocument[] values) {
+ validatePropertyName(name);
+ Parcelable[] documentBundles = new Parcelable[values.length];
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] == null) {
+ throw new IllegalArgumentException("The document at " + i + " is null.");
+ }
+ documentBundles[i] = values[i].mBundle;
+ }
+ mProperties.putParcelableArray(name, documentBundles);
+ }
+
+ /** Builds the {@link GenericDocument} object. */
+ @NonNull
+ public GenericDocument build() {
+ mBuilt = true;
+ // Set current timestamp for creation timestamp by default.
+ if (mBundle.getLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, -1) == -1) {
+ mBundle.putLong(
+ GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
+ System.currentTimeMillis());
+ }
+ return new GenericDocument(mBundle);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mBundle = BundleUtil.deepCopy(mBundle);
+ // mProperties is NonNull and initialized to empty Bundle() in builder.
+ mProperties = Objects.requireNonNull(mBundle.getBundle(PROPERTIES_FIELD));
+ mBuilt = false;
+ }
+ }
+
+ /** Method to ensure property names are not blank */
+ private void validatePropertyName(@NonNull String name) {
+ if (name.isEmpty()) {
+ throw new IllegalArgumentException("Property name cannot be blank.");
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/GetByDocumentIdRequest.java b/android-34/android/app/appsearch/GetByDocumentIdRequest.java
new file mode 100644
index 0000000..b423e67
--- /dev/null
+++ b/android-34/android/app/appsearch/GetByDocumentIdRequest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Encapsulates a request to retrieve documents by namespace and IDs from the {@link
+ * AppSearchSession} database.
+ *
+ * @see AppSearchSession#getByDocumentId
+ */
+public final class GetByDocumentIdRequest {
+ /**
+ * Schema type to be used in {@link GetByDocumentIdRequest.Builder#addProjection} to apply
+ * property paths to all results, excepting any types that have had their own, specific property
+ * paths set.
+ */
+ public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+
+ private final String mNamespace;
+ private final Set<String> mIds;
+ private final Map<String, List<String>> mTypePropertyPathsMap;
+
+ GetByDocumentIdRequest(
+ @NonNull String namespace,
+ @NonNull Set<String> ids,
+ @NonNull Map<String, List<String>> typePropertyPathsMap) {
+ mNamespace = Objects.requireNonNull(namespace);
+ mIds = Objects.requireNonNull(ids);
+ mTypePropertyPathsMap = Objects.requireNonNull(typePropertyPathsMap);
+ }
+
+ /** Returns the namespace attached to the request. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the set of document IDs attached to the request. */
+ @NonNull
+ public Set<String> getIds() {
+ return Collections.unmodifiableSet(mIds);
+ }
+
+ /**
+ * Returns a map from schema type to property paths to be used for projection.
+ *
+ * <p>If the map is empty, then all properties will be retrieved for all results.
+ *
+ * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this
+ * function, rather than calling it multiple times.
+ */
+ @NonNull
+ public Map<String, List<String>> getProjections() {
+ Map<String, List<String>> copy = new ArrayMap<>();
+ for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
+ copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+ }
+ return copy;
+ }
+
+ /**
+ * Returns a map from schema type to property paths to be used for projection.
+ *
+ * <p>If the map is empty, then all properties will be retrieved for all results.
+ *
+ * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this
+ * function, rather than calling it multiple times.
+ */
+ @NonNull
+ public Map<String, List<PropertyPath>> getProjectionPaths() {
+ Map<String, List<PropertyPath>> copy = new ArrayMap<>(mTypePropertyPathsMap.size());
+ for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
+ List<PropertyPath> propertyPathList = new ArrayList<>(entry.getValue().size());
+ for (String p : entry.getValue()) {
+ propertyPathList.add(new PropertyPath(p));
+ }
+ copy.put(entry.getKey(), propertyPathList);
+ }
+ return copy;
+ }
+
+ /**
+ * Returns a map from schema type to property paths to be used for projection.
+ *
+ * <p>If the map is empty, then all properties will be retrieved for all results.
+ *
+ * <p>A more efficient version of {@link #getProjections}, but it returns a modifiable map. This
+ * is not meant to be unhidden and should only be used by internal classes.
+ *
+ * @hide
+ */
+ @NonNull
+ public Map<String, List<String>> getProjectionsInternal() {
+ return mTypePropertyPathsMap;
+ }
+
+ /** Builder for {@link GetByDocumentIdRequest} objects. */
+ public static final class Builder {
+ private final String mNamespace;
+ private ArraySet<String> mIds = new ArraySet<>();
+ private ArrayMap<String, List<String>> mProjectionTypePropertyPaths = new ArrayMap<>();
+ private boolean mBuilt = false;
+
+ /** Creates a {@link GetByDocumentIdRequest.Builder} instance. */
+ public Builder(@NonNull String namespace) {
+ mNamespace = Objects.requireNonNull(namespace);
+ }
+
+ /** Adds one or more document IDs to the request. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addIds(@NonNull String... ids) {
+ Objects.requireNonNull(ids);
+ resetIfBuilt();
+ return addIds(Arrays.asList(ids));
+ }
+
+ /** Adds a collection of IDs to the request. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addIds(@NonNull Collection<String> ids) {
+ Objects.requireNonNull(ids);
+ resetIfBuilt();
+ mIds.addAll(ids);
+ return this;
+ }
+
+ /**
+ * Adds property paths for the specified type to be used for projection. If property paths
+ * are added for a type, then only the properties referred to will be retrieved for results
+ * of that type. If a property path that is specified isn't present in a result, it will be
+ * ignored for that result. Property paths cannot be null.
+ *
+ * <p>If no property paths are added for a particular type, then all properties of results
+ * of that type will be retrieved.
+ *
+ * <p>If property path is added for the {@link
+ * GetByDocumentIdRequest#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will
+ * apply to all results, excepting any types that have their own, specific property paths
+ * set.
+ *
+ * @see SearchSpec.Builder#addProjectionPaths
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addProjection(
+ @NonNull String schemaType, @NonNull Collection<String> propertyPaths) {
+ Objects.requireNonNull(schemaType);
+ Objects.requireNonNull(propertyPaths);
+ resetIfBuilt();
+ List<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
+ for (String propertyPath : propertyPaths) {
+ Objects.requireNonNull(propertyPath);
+ propertyPathsList.add(propertyPath);
+ }
+ mProjectionTypePropertyPaths.put(schemaType, propertyPathsList);
+ return this;
+ }
+
+ /**
+ * Adds property paths for the specified type to be used for projection. If property paths
+ * are added for a type, then only the properties referred to will be retrieved for results
+ * of that type. If a property path that is specified isn't present in a result, it will be
+ * ignored for that result. Property paths cannot be null.
+ *
+ * <p>If no property paths are added for a particular type, then all properties of results
+ * of that type will be retrieved.
+ *
+ * <p>If property path is added for the {@link
+ * GetByDocumentIdRequest#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will
+ * apply to all results, excepting any types that have their own, specific property paths
+ * set.
+ *
+ * @see SearchSpec.Builder#addProjectionPaths
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addProjectionPaths(
+ @NonNull String schemaType, @NonNull Collection<PropertyPath> propertyPaths) {
+ Objects.requireNonNull(schemaType);
+ Objects.requireNonNull(propertyPaths);
+ List<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
+ for (PropertyPath propertyPath : propertyPaths) {
+ propertyPathsList.add(propertyPath.toString());
+ }
+ return addProjection(schemaType, propertyPathsList);
+ }
+
+ /** Builds a new {@link GetByDocumentIdRequest}. */
+ @NonNull
+ public GetByDocumentIdRequest build() {
+ mBuilt = true;
+ return new GetByDocumentIdRequest(mNamespace, mIds, mProjectionTypePropertyPaths);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mIds = new ArraySet<>(mIds);
+ // No need to clone each propertyPathsList inside mProjectionTypePropertyPaths since
+ // the builder only replaces it, never adds to it. So even if the builder is used
+ // again, the previous one will remain with the object.
+ mProjectionTypePropertyPaths = new ArrayMap<>(mProjectionTypePropertyPaths);
+ mBuilt = false;
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/GetSchemaResponse.java b/android-34/android/app/appsearch/GetSchemaResponse.java
new file mode 100644
index 0000000..9591470
--- /dev/null
+++ b/android-34/android/app/appsearch/GetSchemaResponse.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.os.Bundle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** The response class of {@link AppSearchSession#getSchema} */
+public final class GetSchemaResponse {
+ private static final String VERSION_FIELD = "version";
+ private static final String SCHEMAS_FIELD = "schemas";
+ private static final String SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD =
+ "schemasNotDisplayedBySystem";
+ private static final String SCHEMAS_VISIBLE_TO_PACKAGES_FIELD = "schemasVisibleToPackages";
+ private static final String SCHEMAS_VISIBLE_TO_PERMISSION_FIELD = "schemasVisibleToPermissions";
+ private static final String ALL_REQUIRED_PERMISSION_FIELD = "allRequiredPermission";
+ /**
+ * This Set contains all schemas that are not displayed by the system. All values in the set are
+ * prefixed with the package-database prefix. We do lazy fetch, the object will be created when
+ * the user first time fetch it.
+ */
+ @Nullable private Set<String> mSchemasNotDisplayedBySystem;
+ /**
+ * This map contains all schemas and {@link PackageIdentifier} that has access to the schema.
+ * All keys in the map are prefixed with the package-database prefix. We do lazy fetch, the
+ * object will be created when the user first time fetch it.
+ */
+ @Nullable private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+
+ /**
+ * This map contains all schemas and Android Permissions combinations that are required to
+ * access the schema. All keys in the map are prefixed with the package-database prefix. We do
+ * lazy fetch, the object will be created when the user first time fetch it. The Map is
+ * constructed in ANY-ALL cases. The querier could read the {@link GenericDocument} objects
+ * under the {@code schemaType} if they holds ALL required permissions of ANY combinations. The
+ * value set represents {@link
+ * android.app.appsearch.SetSchemaRequest.AppSearchSupportedPermission}.
+ */
+ @Nullable private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
+
+ private final Bundle mBundle;
+
+ GetSchemaResponse(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Returns the overall database schema version.
+ *
+ * <p>If the database is empty, 0 will be returned.
+ */
+ @IntRange(from = 0)
+ public int getVersion() {
+ return mBundle.getInt(VERSION_FIELD);
+ }
+
+ /**
+ * Return the schemas most recently successfully provided to {@link AppSearchSession#setSchema}.
+ *
+ * <p>It is inefficient to call this method repeatedly.
+ */
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public Set<AppSearchSchema> getSchemas() {
+ ArrayList<Bundle> schemaBundles =
+ Objects.requireNonNull(mBundle.getParcelableArrayList(SCHEMAS_FIELD));
+ Set<AppSearchSchema> schemas = new ArraySet<>(schemaBundles.size());
+ for (int i = 0; i < schemaBundles.size(); i++) {
+ schemas.add(new AppSearchSchema(schemaBundles.get(i)));
+ }
+ return schemas;
+ }
+
+ /**
+ * Returns all the schema types that are opted out of being displayed and visible on any system
+ * UI surface.
+ */
+ @NonNull
+ public Set<String> getSchemaTypesNotDisplayedBySystem() {
+ checkGetVisibilitySettingSupported();
+ if (mSchemasNotDisplayedBySystem == null) {
+ List<String> schemasNotDisplayedBySystemList =
+ mBundle.getStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD);
+ mSchemasNotDisplayedBySystem =
+ Collections.unmodifiableSet(new ArraySet<>(schemasNotDisplayedBySystemList));
+ }
+ return mSchemasNotDisplayedBySystem;
+ }
+
+ /**
+ * Returns a mapping of schema types to the set of packages that have access to that schema
+ * type.
+ */
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public Map<String, Set<PackageIdentifier>> getSchemaTypesVisibleToPackages() {
+ checkGetVisibilitySettingSupported();
+ if (mSchemasVisibleToPackages == null) {
+ Bundle schemaVisibleToPackagesBundle =
+ Objects.requireNonNull(mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD));
+ Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
+ for (String key : schemaVisibleToPackagesBundle.keySet()) {
+ List<Bundle> PackageIdentifierBundles =
+ Objects.requireNonNull(
+ schemaVisibleToPackagesBundle.getParcelableArrayList(key));
+ Set<PackageIdentifier> packageIdentifiers =
+ new ArraySet<>(PackageIdentifierBundles.size());
+ for (int i = 0; i < PackageIdentifierBundles.size(); i++) {
+ packageIdentifiers.add(new PackageIdentifier(PackageIdentifierBundles.get(i)));
+ }
+ copy.put(key, packageIdentifiers);
+ }
+ mSchemasVisibleToPackages = Collections.unmodifiableMap(copy);
+ }
+ return mSchemasVisibleToPackages;
+ }
+
+ /**
+ * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
+ * combinations that querier must hold to access that schema type.
+ *
+ * <p>The querier could read the {@link GenericDocument} objects under the {@code schemaType} if
+ * they holds ALL required permissions of ANY of the individual value sets.
+ *
+ * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB}, {
+ * PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+ *
+ * <ul>
+ * <li>A querier holds both PermissionA and PermissionB has access.
+ * <li>A querier holds both PermissionC and PermissionD has access.
+ * <li>A querier holds only PermissionE has access.
+ * <li>A querier holds both PermissionA and PermissionE has access.
+ * <li>A querier holds only PermissionA doesn't have access.
+ * <li>A querier holds both PermissionA and PermissionC doesn't have access.
+ * </ul>
+ *
+ * @return The map contains schema type and all combinations of required permission for querier
+ * to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS}, {@link
+ * SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS}, {@link
+ * SetSchemaRequest#READ_EXTERNAL_STORAGE}, {@link
+ * SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and {@link
+ * SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
+ */
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
+ checkGetVisibilitySettingSupported();
+ if (mSchemasVisibleToPermissions == null) {
+ Map<String, Set<Set<Integer>>> copy = new ArrayMap<>();
+ Bundle schemaVisibleToPermissionBundle =
+ Objects.requireNonNull(mBundle.getBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD));
+ for (String key : schemaVisibleToPermissionBundle.keySet()) {
+ ArrayList<Bundle> allRequiredPermissionsBundle =
+ schemaVisibleToPermissionBundle.getParcelableArrayList(key);
+ Set<Set<Integer>> visibleToPermissions = new ArraySet<>();
+ if (allRequiredPermissionsBundle != null) {
+ // This should never be null
+ for (int i = 0; i < allRequiredPermissionsBundle.size(); i++) {
+ visibleToPermissions.add(
+ new ArraySet<>(
+ allRequiredPermissionsBundle
+ .get(i)
+ .getIntegerArrayList(
+ ALL_REQUIRED_PERMISSION_FIELD)));
+ }
+ }
+ copy.put(key, visibleToPermissions);
+ }
+ mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy);
+ }
+ return mSchemasVisibleToPermissions;
+ }
+
+ private void checkGetVisibilitySettingSupported() {
+ if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) {
+ throw new UnsupportedOperationException(
+ "Get visibility setting is not supported with"
+ + " this backend/Android API level combination.");
+ }
+ }
+
+ /** Builder for {@link GetSchemaResponse} objects. */
+ public static final class Builder {
+ private int mVersion = 0;
+ private ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
+ /**
+ * Creates the object when we actually set them. If we never set visibility settings, we
+ * should throw {@link UnsupportedOperationException} in the visibility getters.
+ */
+ @Nullable private ArrayList<String> mSchemasNotDisplayedBySystem;
+
+ private Bundle mSchemasVisibleToPackages;
+ private Bundle mSchemasVisibleToPermissions;
+ private boolean mBuilt = false;
+
+ /** Create a {@link Builder} object} */
+ public Builder() {
+ this(/*getVisibilitySettingSupported=*/ true);
+ }
+
+ /**
+ * Create a {@link Builder} object}.
+ *
+ * <p>This constructor should only be used in Android API below than T.
+ *
+ * @param getVisibilitySettingSupported whether supported {@link
+ * Features#ADD_PERMISSIONS_AND_GET_VISIBILITY} by this backend/Android API level.
+ * @hide
+ */
+ public Builder(boolean getVisibilitySettingSupported) {
+ if (getVisibilitySettingSupported) {
+ mSchemasNotDisplayedBySystem = new ArrayList<>();
+ mSchemasVisibleToPackages = new Bundle();
+ mSchemasVisibleToPermissions = new Bundle();
+ }
+ }
+
+ /**
+ * Sets the database overall schema version.
+ *
+ * <p>Default version is 0
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setVersion(@IntRange(from = 0) int version) {
+ resetIfBuilt();
+ mVersion = version;
+ return this;
+ }
+
+ /** Adds one {@link AppSearchSchema} to the schema list. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addSchema(@NonNull AppSearchSchema schema) {
+ Objects.requireNonNull(schema);
+ resetIfBuilt();
+ mSchemaBundles.add(schema.getBundle());
+ return this;
+ }
+
+ /**
+ * Sets whether or not documents from the provided {@code schemaType} will be displayed and
+ * visible on any system UI surface.
+ *
+ * @param schemaType The name of an {@link AppSearchSchema} within the same {@link
+ * GetSchemaResponse}, which won't be displayed by system.
+ */
+ // Getter getSchemaTypesNotDisplayedBySystem returns plural objects.
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) {
+ Objects.requireNonNull(schemaType);
+ resetIfBuilt();
+ if (mSchemasNotDisplayedBySystem == null) {
+ mSchemasNotDisplayedBySystem = new ArrayList<>();
+ }
+ mSchemasNotDisplayedBySystem.add(schemaType);
+ return this;
+ }
+
+ /**
+ * Sets whether or not documents from the provided {@code schemaType} can be read by the
+ * specified package.
+ *
+ * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
+ * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+ *
+ * <p>To opt into one-way data sharing with another application, the developer will need to
+ * explicitly grant the other application’s package name and certificate Read access to its
+ * data.
+ *
+ * <p>For two-way data sharing, both applications need to explicitly grant Read access to
+ * one another.
+ *
+ * @param schemaType The schema type to set visibility on.
+ * @param packageIdentifiers Represents the package that has access to the given schema
+ * type.
+ */
+ // Getter getSchemaTypesVisibleToPackages returns a map contains all schema types.
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setSchemaTypeVisibleToPackages(
+ @NonNull String schemaType, @NonNull Set<PackageIdentifier> packageIdentifiers) {
+ Objects.requireNonNull(schemaType);
+ Objects.requireNonNull(packageIdentifiers);
+ resetIfBuilt();
+ ArrayList<Bundle> bundles = new ArrayList<>(packageIdentifiers.size());
+ for (PackageIdentifier packageIdentifier : packageIdentifiers) {
+ bundles.add(packageIdentifier.getBundle());
+ }
+ mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles);
+ return this;
+ }
+
+ /**
+ * Sets a set of required {@link android.Manifest.permission} combinations to the given
+ * schema type.
+ *
+ * <p>The querier could read the {@link GenericDocument} objects under the {@code
+ * schemaType} if they holds ALL required permissions of ANY of the individual value sets.
+ *
+ * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
+ * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+ *
+ * <ul>
+ * <li>A querier holds both PermissionA and PermissionB has access.
+ * <li>A querier holds both PermissionC and PermissionD has access.
+ * <li>A querier holds only PermissionE has access.
+ * <li>A querier holds both PermissionA and PermissionE has access.
+ * <li>A querier holds only PermissionA doesn't have access.
+ * <li>A querier holds both PermissionA and PermissionC doesn't have access.
+ * </ul>
+ *
+ * @see android.Manifest.permission#READ_SMS
+ * @see android.Manifest.permission#READ_CALENDAR
+ * @see android.Manifest.permission#READ_CONTACTS
+ * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
+ * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
+ * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
+ * @param schemaType The schema type to set visibility on.
+ * @param visibleToPermissions The Android permissions that will be required to access the
+ * given schema.
+ */
+ // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setRequiredPermissionsForSchemaTypeVisibility(
+ @NonNull String schemaType,
+ @SetSchemaRequest.AppSearchSupportedPermission @NonNull
+ Set<Set<Integer>> visibleToPermissions) {
+ Objects.requireNonNull(schemaType);
+ Objects.requireNonNull(visibleToPermissions);
+ resetIfBuilt();
+ ArrayList<Bundle> visibleToPermissionsBundle = new ArrayList<>();
+ for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+ for (int permission : allRequiredPermissions) {
+ Preconditions.checkArgumentInRange(
+ permission,
+ SetSchemaRequest.READ_SMS,
+ SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA,
+ "permission");
+ }
+ Bundle allRequiredPermissionsBundle = new Bundle();
+ allRequiredPermissionsBundle.putIntegerArrayList(
+ ALL_REQUIRED_PERMISSION_FIELD, new ArrayList<>(allRequiredPermissions));
+ visibleToPermissionsBundle.add(allRequiredPermissionsBundle);
+ }
+ mSchemasVisibleToPermissions.putParcelableArrayList(
+ schemaType, visibleToPermissionsBundle);
+ return this;
+ }
+
+ /** Builds a {@link GetSchemaResponse} object. */
+ @NonNull
+ public GetSchemaResponse build() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(VERSION_FIELD, mVersion);
+ bundle.putParcelableArrayList(SCHEMAS_FIELD, mSchemaBundles);
+ if (mSchemasNotDisplayedBySystem != null) {
+ // Only save the visibility fields if it was actually set.
+ bundle.putStringArrayList(
+ SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD, mSchemasNotDisplayedBySystem);
+ bundle.putBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD, mSchemasVisibleToPackages);
+ bundle.putBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD, mSchemasVisibleToPermissions);
+ }
+ mBuilt = true;
+ return new GetSchemaResponse(bundle);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mSchemaBundles = new ArrayList<>(mSchemaBundles);
+ if (mSchemasNotDisplayedBySystem != null) {
+ // Only reset the visibility fields if it was actually set.
+ mSchemasNotDisplayedBySystem = new ArrayList<>(mSchemasNotDisplayedBySystem);
+ Bundle copyVisibleToPackages = new Bundle();
+ copyVisibleToPackages.putAll(mSchemasVisibleToPackages);
+ mSchemasVisibleToPackages = copyVisibleToPackages;
+ Bundle copyVisibleToPermissions = new Bundle();
+ copyVisibleToPermissions.putAll(mSchemasVisibleToPermissions);
+ mSchemasVisibleToPermissions = copyVisibleToPermissions;
+ }
+ mBuilt = false;
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/GlobalSearchSession.java b/android-34/android/app/appsearch/GlobalSearchSession.java
new file mode 100644
index 0000000..8073783
--- /dev/null
+++ b/android-34/android/app/appsearch/GlobalSearchSession.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import static android.app.appsearch.SearchSessionUtil.safeExecute;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.IAppSearchManager;
+import android.app.appsearch.aidl.IAppSearchObserverProxy;
+import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.observer.DocumentChangeInfo;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.observer.SchemaChangeInfo;
+import android.content.AttributionSource;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Provides a connection to all AppSearch databases the querying application has been granted access
+ * to.
+ *
+ * <p>This class is thread safe.
+ *
+ * @see AppSearchSession
+ */
+public class GlobalSearchSession implements Closeable {
+ private static final String TAG = "AppSearchGlobalSearchSe";
+
+ private final UserHandle mUserHandle;
+ private final IAppSearchManager mService;
+ private final AttributionSource mCallerAttributionSource;
+
+ // Management of observer callbacks. Key is observed package.
+ @GuardedBy("mObserverCallbacksLocked")
+ private final Map<String, Map<ObserverCallback, IAppSearchObserverProxy>>
+ mObserverCallbacksLocked = new ArrayMap<>();
+
+ private boolean mIsMutated = false;
+ private boolean mIsClosed = false;
+
+ /**
+ * Creates a search session for the client, defined by the {@code userHandle} and
+ * {@code packageName}.
+ */
+ static void createGlobalSearchSession(
+ @NonNull IAppSearchManager service,
+ @NonNull UserHandle userHandle,
+ @NonNull AttributionSource attributionSource,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback) {
+ GlobalSearchSession globalSearchSession = new GlobalSearchSession(service, userHandle,
+ attributionSource);
+ globalSearchSession.initialize(executor, callback);
+ }
+
+ // NOTE: No instance of this class should be created or returned except via initialize().
+ // Once the callback.accept has been called here, the class is ready to use.
+ private void initialize(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback) {
+ try {
+ mService.initialize(
+ mCallerAttributionSource,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ AppSearchResult<Void> result = resultParcel.getResult();
+ if (result.isSuccess()) {
+ callback.accept(
+ AppSearchResult.newSuccessfulResult(
+ GlobalSearchSession.this));
+ } else {
+ callback.accept(AppSearchResult.newFailedResult(result));
+ }
+ });
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private GlobalSearchSession(@NonNull IAppSearchManager service, @NonNull UserHandle userHandle,
+ @NonNull AttributionSource callerAttributionSource) {
+ mService = service;
+ mUserHandle = userHandle;
+ mCallerAttributionSource = callerAttributionSource;
+ }
+
+ /**
+ * Retrieves {@link GenericDocument} documents, belonging to the specified package name and
+ * database name and identified by the namespace and ids in the request, from the
+ * {@link GlobalSearchSession} database.
+ *
+ * <p>If the package or database doesn't exist or if the calling package doesn't have access,
+ * the gets will be handled as failures in an {@link AppSearchBatchResult} object in the
+ * callback.
+ *
+ * @param packageName the name of the package to get from
+ * @param databaseName the name of the database to get from
+ * @param request a request containing a namespace and IDs to get documents for.
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive the pending result of performing this operation. The
+ * keys of the returned {@link AppSearchBatchResult} are the input IDs. The
+ * values are the returned {@link GenericDocument}s on success, or a failed
+ * {@link AppSearchResult} otherwise. IDs that are not found will return a
+ * failed {@link AppSearchResult} with a result code of
+ * {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
+ * occurs in the AppSearch service,
+ * {@link BatchResultCallback#onSystemError} will be invoked with a
+ * {@link Throwable}.
+ */
+ public void getByDocumentId(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull GetByDocumentIdRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BatchResultCallback<String, GenericDocument> callback) {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(databaseName);
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+
+ try {
+ mService.getDocuments(
+ mCallerAttributionSource,
+ /*targetPackageName=*/packageName,
+ databaseName,
+ request.getNamespace(),
+ new ArrayList<>(request.getIds()),
+ request.getProjectionsInternal(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ SearchSessionUtil.createGetDocumentCallback(executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves documents from all AppSearch databases that the querying application has access to.
+ *
+ * <p>Applications can be granted access to documents by specifying {@link
+ * SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage} when building a schema.
+ *
+ * <p>Document access can also be granted to system UIs by specifying {@link
+ * SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem} when building a schema.
+ *
+ * <p>See {@link AppSearchSession#search} for a detailed explanation on forming a query string.
+ *
+ * <p>This method is lightweight. The heavy work will be done in {@link
+ * SearchResults#getNextPage}.
+ *
+ * @param queryExpression query string to search.
+ * @param searchSpec spec for setting document filters, adding projection, setting term
+ * match type, etc.
+ * @return a {@link SearchResults} object for retrieved matched documents.
+ */
+ @NonNull
+ public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ Objects.requireNonNull(queryExpression);
+ Objects.requireNonNull(searchSpec);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+ return new SearchResults(mService, mCallerAttributionSource, /*databaseName=*/null,
+ queryExpression, searchSpec, mUserHandle);
+ }
+
+ /**
+ * Reports that a particular document has been used from a system surface.
+ *
+ * <p>See {@link AppSearchSession#reportUsage} for a general description of document usage, as
+ * well as an API that can be used by the app itself.
+ *
+ * <p>Usage reported via this method is accounted separately from usage reported via
+ * {@link AppSearchSession#reportUsage} and may be accessed using the constants
+ * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and
+ * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}.
+ *
+ * @param request The usage reporting request.
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive errors. If the operation succeeds, the callback will be
+ * invoked with an {@link AppSearchResult} whose value is {@code null}. The
+ * callback will be invoked with an {@link AppSearchResult} of
+ * {@link AppSearchResult#RESULT_SECURITY_ERROR} if this API is invoked by an
+ * app which is not part of the system.
+ */
+ public void reportSystemUsage(
+ @NonNull ReportSystemUsageRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<Void>> callback) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+ try {
+ mService.reportUsage(
+ mCallerAttributionSource,
+ request.getPackageName(),
+ request.getDatabaseName(),
+ request.getNamespace(),
+ request.getDocumentId(),
+ request.getUsageTimestampMillis(),
+ /*systemUsage=*/ true,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(
+ executor,
+ callback,
+ () -> callback.accept(resultParcel.getResult()));
+ }
+ });
+ mIsMutated = true;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves the collection of schemas most recently successfully provided to {@link
+ * AppSearchSession#setSchema} for any types belonging to the requested package and database
+ * that the caller has been granted access to.
+ *
+ * <p>If the requested package/database combination does not exist or the caller has not been
+ * granted access to it, then an empty GetSchemaResponse will be returned.
+ *
+ * @param packageName the package that owns the requested {@link AppSearchSchema} instances.
+ * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.
+ * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has
+ * access to or an empty GetSchemaResponse if the request package and database does not
+ * exist, has not set a schema or contains no schemas that are accessible to the caller.
+ */
+ // This call hits disk; async API prevents us from treating these calls as properties.
+ public void getSchema(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<GetSchemaResponse>> callback) {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(databaseName);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+ try {
+ mService.getSchema(
+ mCallerAttributionSource,
+ packageName,
+ databaseName,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ AppSearchResult<Bundle> result = resultParcel.getResult();
+ if (result.isSuccess()) {
+ GetSchemaResponse response = new GetSchemaResponse(
+ Objects.requireNonNull(result.getResultValue()));
+ callback.accept(AppSearchResult.newSuccessfulResult(response));
+ } else {
+ callback.accept(AppSearchResult.newFailedResult(result));
+ }
+ });
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Adds an {@link ObserverCallback} to monitor changes within the databases owned by
+ * {@code targetPackageName} if they match the given
+ * {@link android.app.appsearch.observer.ObserverSpec}.
+ *
+ * <p>The observer callback is only triggered for data that changes after it is registered. No
+ * notification about existing data is sent as a result of registering an observer. To find out
+ * about existing data, you must use the {@link GlobalSearchSession#search} API.
+ *
+ * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} changes its schema visibility settings.
+ *
+ * <p>If no package matching {@code targetPackageName} exists on the system, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} is installed and starts indexing data.
+ *
+ * @param targetPackageName Package whose changes to monitor
+ * @param spec Specification of what types of changes to listen for
+ * @param executor Executor on which to call the {@code observer} callback methods.
+ * @param observer Callback to trigger when a schema or document changes
+ * @throws AppSearchException If an unexpected error occurs when trying to register an observer.
+ */
+ public void registerObserverCallback(
+ @NonNull String targetPackageName,
+ @NonNull ObserverSpec spec,
+ @NonNull Executor executor,
+ @NonNull ObserverCallback observer) throws AppSearchException {
+ Objects.requireNonNull(targetPackageName);
+ Objects.requireNonNull(spec);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(observer);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+
+ synchronized (mObserverCallbacksLocked) {
+ IAppSearchObserverProxy stub = null;
+ Map<ObserverCallback, IAppSearchObserverProxy> observersForPackage =
+ mObserverCallbacksLocked.get(targetPackageName);
+ if (observersForPackage != null) {
+ stub = observersForPackage.get(observer);
+ }
+ if (stub == null) {
+ // No stub is associated with this package and observer, so we must create one.
+ stub = new IAppSearchObserverProxy.Stub() {
+ @Override
+ public void onSchemaChanged(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull List<String> changedSchemaNames) {
+ safeExecute(executor, this::suppressingErrorCallback, () -> {
+ SchemaChangeInfo changeInfo = new SchemaChangeInfo(
+ packageName, databaseName, new ArraySet<>(changedSchemaNames));
+ observer.onSchemaChanged(changeInfo);
+ });
+ }
+
+ @Override
+ public void onDocumentChanged(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull String namespace,
+ @NonNull String schemaName,
+ @NonNull List<String> changedDocumentIds) {
+ safeExecute(executor, this::suppressingErrorCallback, () -> {
+ DocumentChangeInfo changeInfo = new DocumentChangeInfo(
+ packageName,
+ databaseName,
+ namespace,
+ schemaName,
+ new ArraySet<>(changedDocumentIds));
+ observer.onDocumentChanged(changeInfo);
+ });
+ }
+
+ /**
+ * Error-handling callback that simply drops errors.
+ *
+ * <p>If we fail to deliver change notifications, there isn't much we can do.
+ * The API doesn't allow the user to provide a callback to invoke on failure of
+ * change notification delivery. {@link SearchSessionUtil#safeExecute} already
+ * includes a log message. So we just do nothing.
+ */
+ private void suppressingErrorCallback(@NonNull AppSearchResult<?> unused) {
+ }
+ };
+ }
+
+ // Regardless of whether this stub was fresh or not, we have to register it again
+ // because the user might be supplying a different spec.
+ AppSearchResultParcel<Void> resultParcel;
+ try {
+ resultParcel = mService.registerObserverCallback(
+ mCallerAttributionSource,
+ targetPackageName,
+ spec.getBundle(),
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ stub);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ // See whether registration was successful
+ AppSearchResult<Void> result = resultParcel.getResult();
+ if (!result.isSuccess()) {
+ throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
+ }
+
+ // Now that registration has succeeded, save this stub into our in-memory cache. This
+ // isn't done when errors occur because the user may not call unregisterObserverCallback
+ // if registerObserverCallback threw.
+ if (observersForPackage == null) {
+ observersForPackage = new ArrayMap<>();
+ mObserverCallbacksLocked.put(targetPackageName, observersForPackage);
+ }
+ observersForPackage.put(observer, stub);
+ }
+ }
+
+ /**
+ * Removes previously registered {@link ObserverCallback} instances from the system.
+ *
+ * <p>All instances of {@link ObserverCallback} which are registered to observe
+ * {@code targetPackageName} and compare equal to the provided callback using the provided
+ * argument's {@code ObserverCallback#equals} will be removed.
+ *
+ * <p>If no matching observers have been registered, this method has no effect. If multiple
+ * matching observers have been registered, all will be removed.
+ *
+ * @param targetPackageName Package which the observers to be removed are listening to.
+ * @param observer Callback to unregister.
+ * @throws AppSearchException if an error occurs trying to remove the observer, such as a
+ * failure to communicate with the system service. Note that no error
+ * will be thrown if the provided observer doesn't match any
+ * registered observer.
+ */
+ public void unregisterObserverCallback(
+ @NonNull String targetPackageName,
+ @NonNull ObserverCallback observer) throws AppSearchException {
+ Objects.requireNonNull(targetPackageName);
+ Objects.requireNonNull(observer);
+ Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+
+ IAppSearchObserverProxy stub;
+ synchronized (mObserverCallbacksLocked) {
+ Map<ObserverCallback, IAppSearchObserverProxy> observersForPackage =
+ mObserverCallbacksLocked.get(targetPackageName);
+ if (observersForPackage == null) {
+ return; // No observers registered for this package. Nothing to do.
+ }
+ stub = observersForPackage.get(observer);
+ if (stub == null) {
+ return; // No such observer registered. Nothing to do.
+ }
+
+ AppSearchResultParcel<Void> resultParcel;
+ try {
+ resultParcel = mService.unregisterObserverCallback(
+ mCallerAttributionSource,
+ targetPackageName,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+ stub);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ AppSearchResult<Void> result = resultParcel.getResult();
+ if (!result.isSuccess()) {
+ throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
+ }
+
+ // Only remove from the in-memory map once removal from the service side succeeds
+ observersForPackage.remove(observer);
+ if (observersForPackage.isEmpty()) {
+ mObserverCallbacksLocked.remove(targetPackageName);
+ }
+ }
+ }
+
+ /**
+ * Closes the {@link GlobalSearchSession}. Persists all mutations, including usage reports, to
+ * disk.
+ */
+ @Override
+ public void close() {
+ if (mIsMutated && !mIsClosed) {
+ try {
+ mService.persistToDisk(
+ mCallerAttributionSource,
+ mUserHandle,
+ /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime());
+ mIsClosed = true;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to close the GlobalSearchSession", e);
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/InternalSetSchemaResponse.java b/android-34/android/app/appsearch/InternalSetSchemaResponse.java
new file mode 100644
index 0000000..0094f47
--- /dev/null
+++ b/android-34/android/app/appsearch/InternalSetSchemaResponse.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+import java.util.Objects;
+
+/**
+ * An internal wrapper class of {@link SetSchemaResponse}.
+ *
+ * <p>For public users, if the {@link android.app.appsearch.AppSearchSession#setSchema} failed, we
+ * will directly throw an Exception. But AppSearch internal need to divert the incompatible changes
+ * form other call flows. This class adds a {@link #isSuccess()} to indicate if the call fails
+ * because of incompatible change.
+ *
+ * @hide
+ */
+public class InternalSetSchemaResponse {
+
+ private static final String IS_SUCCESS_FIELD = "isSuccess";
+ private static final String SET_SCHEMA_RESPONSE_BUNDLE_FIELD = "setSchemaResponseBundle";
+ private static final String ERROR_MESSAGE_FIELD = "errorMessage";
+
+ private final Bundle mBundle;
+
+ public InternalSetSchemaResponse(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ private InternalSetSchemaResponse(
+ boolean isSuccess,
+ @NonNull SetSchemaResponse setSchemaResponse,
+ @Nullable String errorMessage) {
+ Objects.requireNonNull(setSchemaResponse);
+ mBundle = new Bundle();
+ mBundle.putBoolean(IS_SUCCESS_FIELD, isSuccess);
+ mBundle.putBundle(SET_SCHEMA_RESPONSE_BUNDLE_FIELD, setSchemaResponse.getBundle());
+ mBundle.putString(ERROR_MESSAGE_FIELD, errorMessage);
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates a new successful {@link InternalSetSchemaResponse}.
+ *
+ * @param setSchemaResponse The object this internal object represents.
+ */
+ @NonNull
+ public static InternalSetSchemaResponse newSuccessfulSetSchemaResponse(
+ @NonNull SetSchemaResponse setSchemaResponse) {
+ return new InternalSetSchemaResponse(
+ /*isSuccess=*/ true, setSchemaResponse, /*errorMessage=*/ null);
+ }
+
+ /**
+ * Creates a new failed {@link InternalSetSchemaResponse}.
+ *
+ * @param setSchemaResponse The object this internal object represents.
+ * @param errorMessage An string describing the reason or nature of the failure.
+ */
+ @NonNull
+ public static InternalSetSchemaResponse newFailedSetSchemaResponse(
+ @NonNull SetSchemaResponse setSchemaResponse, @NonNull String errorMessage) {
+ return new InternalSetSchemaResponse(/*isSuccess=*/ false, setSchemaResponse, errorMessage);
+ }
+
+ /** Returns {@code true} if the schema request is proceeded successfully. */
+ public boolean isSuccess() {
+ return mBundle.getBoolean(IS_SUCCESS_FIELD);
+ }
+
+ /**
+ * Returns the {@link SetSchemaResponse} of the set schema call.
+ *
+ * <p>The call may or may not success. Check {@link #isSuccess()} before call this method.
+ */
+ @NonNull
+ public SetSchemaResponse getSetSchemaResponse() {
+ return new SetSchemaResponse(mBundle.getBundle(SET_SCHEMA_RESPONSE_BUNDLE_FIELD));
+ }
+
+ /**
+ * Returns the error message associated with this response.
+ *
+ * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}.
+ */
+ @Nullable
+ public String getErrorMessage() {
+ return mBundle.getString(ERROR_MESSAGE_FIELD);
+ }
+}
diff --git a/android-34/android/app/appsearch/JoinSpec.java b/android-34/android/app/appsearch/JoinSpec.java
new file mode 100644
index 0000000..1384898
--- /dev/null
+++ b/android-34/android/app/appsearch/JoinSpec.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.os.Bundle;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * This class represents the specifications for the joining operation in search.
+ *
+ * <p>Joins are only possible for matching on the qualified id of an outer document and a property
+ * value within a subquery document. In the subquery documents, these values may be referred to with
+ * a property path such as "email.recipient.id" or "entityId" or a property expression. One such
+ * property expression is "this.qualifiedId()", which refers to the document's combined package,
+ * database, namespace, and id.
+ *
+ * <p>Take these outer query and subquery results for example:
+ *
+ * <pre>{@code
+ * Outer result {
+ * id: id1
+ * score: 5
+ * }
+ * Subquery result 1 {
+ * id: id2
+ * score: 2
+ * entityId: pkg$db/ns#id1
+ * notes: This is some doc
+ * }
+ * Subquery result 2 {
+ * id: id3
+ * score: 3
+ * entityId: pkg$db/ns#id2
+ * notes: This is another doc
+ * }
+ * }</pre>
+ *
+ * <p>In this example, subquery result 1 contains a property "entityId" whose value is
+ * "pkg$db/ns#id1", referring to the outer result. If you call {@link Builder} with "entityId", we
+ * will retrieve the value of the property "entityId" from the child document, which is
+ * "pkg$db#ns/id1". Let's say the qualified id of the outer result is "pkg$db#ns/id1". This would
+ * mean the subquery result 1 document will be matched to that parent document. This is done by
+ * adding a {@link SearchResult} containing the child document to the top-level parent {@link
+ * SearchResult#getJoinedResults}.
+ *
+ * <p>If {@link #getChildPropertyExpression} is "notes", we will check the values of the notes
+ * property in the subquery results. In subquery result 1, this values is "This is some doc", which
+ * does not equal the qualified id of the outer query result. As such, subquery result 1 will not be
+ * joined to the outer query result.
+ *
+ * <p>In terms of scoring, if {@link SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} is set in
+ * {@link SearchSpec#getRankingStrategy}, the scores of the outer SearchResults can be influenced by
+ * the ranking signals of the subquery results. For example, if the {@link
+ * JoinSpec#getAggregationScoringStrategy} is set to {@link
+ * JoinSpec#AGGREGATION_SCORING_MIN_RANKING_SIGNAL}, the ranking signal of the outer {@link
+ * SearchResult} will be set to the minimum of the ranking signals of the subquery results. In this
+ * case, it will be the minimum of 2 and 3, which is 2. If the {@link
+ * JoinSpec#getAggregationScoringStrategy} is set to {@link
+ * JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, the ranking signal of the outer {@link
+ * SearchResult} will stay as it is.
+ */
+// TODO(b/256022027): Update javadoc once "Joinable"/"qualifiedId" type is added to reflect the
+// fact that childPropertyExpression has to point to property of that type.
+public final class JoinSpec {
+ static final String NESTED_QUERY = "nestedQuery";
+ static final String NESTED_SEARCH_SPEC = "nestedSearchSpec";
+ static final String CHILD_PROPERTY_EXPRESSION = "childPropertyExpression";
+ static final String MAX_JOINED_RESULT_COUNT = "maxJoinedResultCount";
+ static final String AGGREGATION_SCORING_STRATEGY = "aggregationScoringStrategy";
+
+ private static final int DEFAULT_MAX_JOINED_RESULT_COUNT = 10;
+
+ /**
+ * A property expression referring to the combined package name, database name, namespace, and
+ * id of the document.
+ *
+ * <p>For instance, if a document with an id of "id1" exists in the namespace "ns" within the
+ * database "db" created by package "pkg", this would evaluate to "pkg$db/ns#id1".
+ *
+ * @hide
+ */
+ public static final String QUALIFIED_ID = "this.qualifiedId()";
+
+ /**
+ * Aggregation scoring strategy for join spec.
+ *
+ * @hide
+ */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link JoinSpecProto.AggregationScoreStrategy.Code}
+ @IntDef(
+ value = {
+ AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL,
+ AGGREGATION_SCORING_RESULT_COUNT,
+ AGGREGATION_SCORING_MIN_RANKING_SIGNAL,
+ AGGREGATION_SCORING_AVG_RANKING_SIGNAL,
+ AGGREGATION_SCORING_MAX_RANKING_SIGNAL,
+ AGGREGATION_SCORING_SUM_RANKING_SIGNAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AggregationScoringStrategy {}
+
+ /**
+ * Do not score the aggregation of joined documents. This is for the case where we want to
+ * perform a join, but keep the parent ranking signal.
+ */
+ public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0;
+ /** Score the aggregation of joined documents by counting the number of results. */
+ public static final int AGGREGATION_SCORING_RESULT_COUNT = 1;
+ /** Score the aggregation of joined documents using the smallest ranking signal. */
+ public static final int AGGREGATION_SCORING_MIN_RANKING_SIGNAL = 2;
+ /** Score the aggregation of joined documents using the average ranking signal. */
+ public static final int AGGREGATION_SCORING_AVG_RANKING_SIGNAL = 3;
+ /** Score the aggregation of joined documents using the largest ranking signal. */
+ public static final int AGGREGATION_SCORING_MAX_RANKING_SIGNAL = 4;
+ /** Score the aggregation of joined documents using the sum of ranking signal. */
+ public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5;
+
+ private final Bundle mBundle;
+
+ /** @hide */
+ public JoinSpec(@NonNull Bundle bundle) {
+ Objects.requireNonNull(bundle);
+ mBundle = bundle;
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /** Returns the query to run on the joined documents. */
+ @NonNull
+ public String getNestedQuery() {
+ return mBundle.getString(NESTED_QUERY);
+ }
+
+ /**
+ * The property expression that is used to get values from child documents, returned from the
+ * nested search. These values are then used to match them to parent documents. These are
+ * analogous to foreign keys.
+ *
+ * @return the property expression to match in the child documents.
+ * @see Builder
+ */
+ @NonNull
+ public String getChildPropertyExpression() {
+ return mBundle.getString(CHILD_PROPERTY_EXPRESSION);
+ }
+
+ /**
+ * Returns the max amount of {@link SearchResult} objects to return with the parent document,
+ * with a default of 10 SearchResults.
+ */
+ public int getMaxJoinedResultCount() {
+ return mBundle.getInt(MAX_JOINED_RESULT_COUNT);
+ }
+
+ /**
+ * Returns the search spec used to retrieve the joined documents.
+ *
+ * <p>If {@link Builder#setNestedSearch} is never called, this will return a {@link SearchSpec}
+ * with all default values. This will match every document, as the nested search query will be
+ * "" and no schema will be filtered out.
+ */
+ @NonNull
+ public SearchSpec getNestedSearchSpec() {
+ return new SearchSpec(mBundle.getBundle(NESTED_SEARCH_SPEC));
+ }
+
+ /**
+ * Gets the joined document list scoring strategy.
+ *
+ * <p>The default scoring strategy is {@link #AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL},
+ * which specifies that the score of the outer parent document will be used.
+ *
+ * @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
+ */
+ @AggregationScoringStrategy
+ public int getAggregationScoringStrategy() {
+ return mBundle.getInt(AGGREGATION_SCORING_STRATEGY);
+ }
+
+ /** Builder for {@link JoinSpec objects}. */
+ public static final class Builder {
+
+ // The default nested SearchSpec.
+ private static final SearchSpec EMPTY_SEARCH_SPEC = new SearchSpec.Builder().build();
+
+ private String mNestedQuery = "";
+ private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC;
+ private final String mChildPropertyExpression;
+ private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT;
+
+ @AggregationScoringStrategy
+ private int mAggregationScoringStrategy = AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL;
+
+ /**
+ * Create a specification for the joining operation in search.
+ *
+ * <p>The child property expressions Specifies how to join documents. Documents with a child
+ * property expression equal to the qualified id of the parent will be retrieved.
+ *
+ * <p>Property expressions differ from {@link PropertyPath} as property expressions may
+ * refer to document properties or nested document properties such as "person.business.id"
+ * as well as a property expression. Currently the only property expression is
+ * "this.qualifiedId()". {@link PropertyPath} objects may only reference document properties
+ * and nested document properties.
+ *
+ * <p>In order to join a child document to a parent document, the child document must
+ * contain the parent's qualified id at the property expression specified by this method.
+ *
+ * @param childPropertyExpression the property to match in the child documents.
+ */
+ // TODO(b/256022027): Reword comments to reference either "expression" or "PropertyPath"
+ // once wording is finalized.
+ // TODO(b/256022027): Add another method to allow providing PropertyPath objects as
+ // equality constraints.
+ // TODO(b/256022027): Change to allow for multiple child property expressions if multiple
+ // parent property expressions get supported.
+ public Builder(@NonNull String childPropertyExpression) {
+ Objects.requireNonNull(childPropertyExpression);
+ mChildPropertyExpression = childPropertyExpression;
+ }
+
+ /**
+ * Further filters the documents being joined.
+ *
+ * <p>If this method is never called, {@link JoinSpec#getNestedQuery} will return an empty
+ * string, meaning we will join with every possible document that matches the equality
+ * constraints and hasn't been filtered out by the type or namespace filters.
+ *
+ * @see JoinSpec#getNestedQuery
+ * @see JoinSpec#getNestedSearchSpec
+ */
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // See getNestedQuery & getNestedSearchSpec
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setNestedSearch(
+ @NonNull String nestedQuery, @NonNull SearchSpec nestedSearchSpec) {
+ Objects.requireNonNull(nestedQuery);
+ Objects.requireNonNull(nestedSearchSpec);
+ mNestedQuery = nestedQuery;
+ mNestedSearchSpec = nestedSearchSpec;
+
+ return this;
+ }
+
+ /**
+ * Sets the max amount of {@link SearchResults} to return with the parent document, with a
+ * default of 10 SearchResults.
+ *
+ * <p>This does NOT limit the number of results that are joined with the parent document for
+ * scoring. This means that, when set, only a maximum of {@code maxJoinedResultCount}
+ * results will be returned with each parent document, but all results that are joined with
+ * a parent will factor into the score.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setMaxJoinedResultCount(int maxJoinedResultCount) {
+ mMaxJoinedResultCount = maxJoinedResultCount;
+ return this;
+ }
+
+ /**
+ * Sets how we derive a single score from a list of joined documents.
+ *
+ * <p>The default scoring strategy is {@link
+ * #AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, which specifies that the ranking
+ * signal of the outer parent document will be used.
+ *
+ * @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setAggregationScoringStrategy(
+ @AggregationScoringStrategy int aggregationScoringStrategy) {
+ Preconditions.checkArgumentInRange(
+ aggregationScoringStrategy,
+ AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL,
+ AGGREGATION_SCORING_SUM_RANKING_SIGNAL,
+ "aggregationScoringStrategy");
+ mAggregationScoringStrategy = aggregationScoringStrategy;
+ return this;
+ }
+
+ /** Constructs a new {@link JoinSpec} from the contents of this builder. */
+ @NonNull
+ public JoinSpec build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(NESTED_QUERY, mNestedQuery);
+ bundle.putBundle(NESTED_SEARCH_SPEC, mNestedSearchSpec.getBundle());
+ bundle.putString(CHILD_PROPERTY_EXPRESSION, mChildPropertyExpression);
+ bundle.putInt(MAX_JOINED_RESULT_COUNT, mMaxJoinedResultCount);
+ bundle.putInt(AGGREGATION_SCORING_STRATEGY, mAggregationScoringStrategy);
+ return new JoinSpec(bundle);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/Migrator.java b/android-34/android/app/appsearch/Migrator.java
new file mode 100644
index 0000000..c5a0f63
--- /dev/null
+++ b/android-34/android/app/appsearch/Migrator.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+
+/**
+ * A migrator class to translate {@link GenericDocument} from different version of {@link
+ * AppSearchSchema}
+ *
+ * <p>Make non-backwards-compatible changes will delete all stored documents in old schema. You can
+ * save your documents by setting {@link Migrator} via the {@link
+ * SetSchemaRequest.Builder#setMigrator} for each type and target version you want to save.
+ *
+ * <p>{@link #onDowngrade} or {@link #onUpgrade} will be triggered if the version number of the
+ * schema stored in AppSearch is different with the version in the request.
+ *
+ * <p>If any error or Exception occurred in the {@link #onDowngrade} or {@link #onUpgrade}, all the
+ * setSchema request will be rejected unless the schema changes are backwards-compatible, and stored
+ * documents won't have any observable changes.
+ */
+public abstract class Migrator {
+ /**
+ * Returns {@code true} if this migrator's source type needs to be migrated to update from
+ * currentVersion to finalVersion.
+ *
+ * <p>Migration won't be triggered if currentVersion is equal to finalVersion even if {@link
+ * #shouldMigrate} return true;
+ */
+ public abstract boolean shouldMigrate(int currentVersion, int finalVersion);
+
+ /**
+ * Migrates {@link GenericDocument} to a newer version of {@link AppSearchSchema}.
+ *
+ * <p>This method will be invoked only if the {@link SetSchemaRequest} is setting a higher
+ * version number than the current {@link AppSearchSchema} saved in AppSearch.
+ *
+ * <p>If this {@link Migrator} is provided to cover a compatible schema change via {@link
+ * AppSearchSession#setSchema}, documents under the old version won't be removed unless you use
+ * the same document ID.
+ *
+ * <p>This method will be invoked on the background worker thread provided via {@link
+ * AppSearchSession#setSchema}.
+ *
+ * @param currentVersion The current version of the document's schema.
+ * @param finalVersion The final version that documents need to be migrated to.
+ * @param document The {@link GenericDocument} need to be translated to new version.
+ * @return A {@link GenericDocument} in new version.
+ */
+ @WorkerThread
+ @NonNull
+ public abstract GenericDocument onUpgrade(
+ int currentVersion, int finalVersion, @NonNull GenericDocument document);
+
+ /**
+ * Migrates {@link GenericDocument} to an older version of {@link AppSearchSchema}.
+ *
+ * <p>This method will be invoked only if the {@link SetSchemaRequest} is setting a lower
+ * version number than the current {@link AppSearchSchema} saved in AppSearch.
+ *
+ * <p>If this {@link Migrator} is provided to cover a compatible schema change via {@link
+ * AppSearchSession#setSchema}, documents under the old version won't be removed unless you use
+ * the same document ID.
+ *
+ * <p>This method will be invoked on the background worker thread.
+ *
+ * @param currentVersion The current version of the document's schema.
+ * @param finalVersion The final version that documents need to be migrated to.
+ * @param document The {@link GenericDocument} need to be translated to new version.
+ * @return A {@link GenericDocument} in new version.
+ */
+ @WorkerThread
+ @NonNull
+ public abstract GenericDocument onDowngrade(
+ int currentVersion, int finalVersion, @NonNull GenericDocument document);
+}
diff --git a/android-34/android/app/appsearch/PackageIdentifier.java b/android-34/android/app/appsearch/PackageIdentifier.java
new file mode 100644
index 0000000..0aed720
--- /dev/null
+++ b/android-34/android/app/appsearch/PackageIdentifier.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.util.BundleUtil;
+import android.os.Bundle;
+
+import java.util.Objects;
+
+/** This class represents a uniquely identifiable package. */
+public class PackageIdentifier {
+ private static final String PACKAGE_NAME_FIELD = "packageName";
+ private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";
+
+ private final Bundle mBundle;
+
+ /**
+ * Creates a unique identifier for a package.
+ *
+ * <p>SHA-256 certificate digests for a signed application can be retrieved with the <a
+ * href="{@docRoot}studio/command-line/apksigner/">apksigner tool</a> that is part of the
+ * Android SDK build tools. Use {@code apksigner verify --print-certs path/to/apk.apk} to
+ * retrieve the SHA-256 certificate digest for the target application. Once retrieved, the
+ * SHA-256 certificate digest should be converted to a {@code byte[]} by decoding it in base16:
+ *
+ * <pre>
+ * new android.content.pm.Signature(outputDigest).toByteArray();
+ * </pre>
+ *
+ * @param packageName Name of the package.
+ * @param sha256Certificate SHA-256 certificate digest of the package.
+ */
+ public PackageIdentifier(@NonNull String packageName, @NonNull byte[] sha256Certificate) {
+ mBundle = new Bundle();
+ mBundle.putString(PACKAGE_NAME_FIELD, packageName);
+ mBundle.putByteArray(SHA256_CERTIFICATE_FIELD, sha256Certificate);
+ }
+
+ /** @hide */
+ public PackageIdentifier(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ /** @hide */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ @NonNull
+ public String getPackageName() {
+ return Objects.requireNonNull(mBundle.getString(PACKAGE_NAME_FIELD));
+ }
+
+ @NonNull
+ public byte[] getSha256Certificate() {
+ return Objects.requireNonNull(mBundle.getByteArray(SHA256_CERTIFICATE_FIELD));
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof PackageIdentifier)) {
+ return false;
+ }
+ final PackageIdentifier other = (PackageIdentifier) obj;
+ return BundleUtil.deepEquals(mBundle, other.mBundle);
+ }
+
+ @Override
+ public int hashCode() {
+ return BundleUtil.deepHashCode(mBundle);
+ }
+}
diff --git a/android-34/android/app/appsearch/ParcelableUtil.java b/android-34/android/app/appsearch/ParcelableUtil.java
new file mode 100644
index 0000000..dc7183c
--- /dev/null
+++ b/android-34/android/app/appsearch/ParcelableUtil.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/** Wrapper class to provide implementation for readBlob/writeBlob for all API levels.
+ *
+ * @hide
+ */
+public class ParcelableUtil {
+ private static final String TAG = "AppSearchParcel";
+ private static final String TEMP_FILE_PREFIX = "AppSearchSerializedBytes";
+ private static final String TEMP_FILE_SUFFIX = ".tmp";
+ // Same as IBinder.MAX_IPC_LIMIT. Limit that should be placed on IPC sizes to keep them safely
+ // under the transaction buffer limit.
+ private static final int DOCUMENT_SIZE_LIMIT_IN_BYTES = 64 * 1024;
+
+
+ // TODO(b/232805516): Update SDK_INT in Android.bp to safeguard from unexpected compiler issues.
+ @SuppressLint("ObsoleteSdkInt")
+ public static void writeBlob(@NonNull Parcel parcel, @NonNull byte[] bytes) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // Since parcel.writeBlob was added in API level 33, it is not available
+ // on lower API levels.
+ parcel.writeBlob(bytes);
+ } else {
+ writeToParcelForSAndBelow(parcel, bytes);
+ }
+ }
+
+ private static void writeToParcelForSAndBelow(Parcel parcel, byte[] bytes) {
+ try {
+ parcel.writeInt(bytes.length);
+ if (bytes.length <= DOCUMENT_SIZE_LIMIT_IN_BYTES) {
+ parcel.writeByteArray(bytes);
+ } else {
+ ParcelFileDescriptor parcelFileDescriptor =
+ writeDataToTempFileAndUnlinkFile(bytes);
+ parcel.writeFileDescriptor(parcelFileDescriptor.getFileDescriptor());
+ }
+ } catch (IOException e) {
+ // TODO(b/232805516): Add abstraction to handle the exception based on environment.
+ Log.w(TAG, "Couldn't write to unlinked file.", e);
+ }
+ }
+
+ @NonNull
+ // TODO(b/232805516): Update SDK_INT in Android.bp to safeguard from unexpected compiler issues.
+ @SuppressLint("ObsoleteSdkInt")
+ public static byte[] readBlob(Parcel parcel) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // Since parcel.readBlob was added in API level 33, it is not available
+ // on lower API levels.
+ return parcel.readBlob();
+ } else {
+ return readFromParcelForSAndBelow(parcel);
+ }
+ }
+
+ private static byte[] readFromParcelForSAndBelow(Parcel parcel) {
+ try {
+ int length = parcel.readInt();
+ if (length <= DOCUMENT_SIZE_LIMIT_IN_BYTES) {
+ byte[] documentByteArray = new byte[length];
+ parcel.readByteArray(documentByteArray);
+ return documentByteArray;
+ } else {
+ ParcelFileDescriptor pfd = parcel.readFileDescriptor();
+ return getDataFromFd(pfd, length);
+ }
+ } catch (IOException e) {
+ // TODO(b/232805516): Add abstraction to handle the exception based on environment.
+ Log.w(TAG, "Couldn't read from unlinked file.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Reads data bytes from file using provided FileDescriptor. It also closes the PFD so that
+ * will delete the underlying file if it's the only reference left.
+ *
+ * @param pfd ParcelFileDescriptor for the file to read.
+ * @param length Number of bytes to read from the file.
+ */
+ private static byte[] getDataFromFd(ParcelFileDescriptor pfd,
+ int length) throws IOException {
+ try(DataInputStream in =
+ new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(pfd))){
+ byte[] data = new byte[length];
+ in.read(data);
+ return data;
+ }
+ }
+
+ /**
+ * Writes to a temp file owned by the caller, then unlinks/deletes it, and returns an FD which
+ * is the only remaining reference to the tmp file.
+ *
+ * @param data Data in the form of byte array to write to the file.
+ */
+ private static ParcelFileDescriptor writeDataToTempFileAndUnlinkFile(byte[] data)
+ throws IOException {
+ // TODO(b/232959004): Update directory to app-specific cache dir instead of null.
+ File unlinkedFile =
+ File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX, /* directory= */ null);
+ try(DataOutputStream out = new DataOutputStream(new FileOutputStream(unlinkedFile))) {
+ out.write(data);
+ out.flush();
+ }
+ ParcelFileDescriptor parcelFileDescriptor =
+ ParcelFileDescriptor.open(unlinkedFile,
+ ParcelFileDescriptor.MODE_CREATE
+ | ParcelFileDescriptor.MODE_READ_WRITE);
+ unlinkedFile.delete();
+ return parcelFileDescriptor;
+ }
+
+ private ParcelableUtil() {}
+}
diff --git a/android-34/android/app/appsearch/PropertyPath.java b/android-34/android/app/appsearch/PropertyPath.java
new file mode 100644
index 0000000..9fb5280
--- /dev/null
+++ b/android-34/android/app/appsearch/PropertyPath.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a property path returned from searching the AppSearch Database.
+ *
+ * <p>When searching the AppSearch Database, you will get back {@link SearchResult.MatchInfo}
+ * objects that contain a property path signifying the location of a match within the database. This
+ * is a string that may look something like "foo.bar[0]". {@link PropertyPath} parses this string
+ * and breaks it up into a List of {@link PathSegment}s. These may represent either a property or a
+ * property and a 0-based index into the property. For instance, "foo.bar[1]" would be parsed into a
+ * {@link PathSegment} with a property name of foo and a {@link PathSegment} with a property name of
+ * bar and an index of 1. This allows for easier manipulation of the property path.
+ *
+ * <p>This class won't perform any retrievals, it will only parse the path string. As such, it may
+ * not necessarily refer to a valid path in the database.
+ *
+ * @see SearchResult.MatchInfo
+ */
+public class PropertyPath implements Iterable<PropertyPath.PathSegment> {
+ private final List<PathSegment> mPathList;
+
+ /**
+ * Constructor directly accepting a path list
+ *
+ * @param pathList a list of PathSegments
+ */
+ public PropertyPath(@NonNull List<PathSegment> pathList) {
+ mPathList = new ArrayList<>(pathList);
+ }
+
+ /**
+ * Constructor that parses a string representing the path to populate a List of PathSegments
+ *
+ * @param path the string to be validated and parsed into PathSegments
+ * @throws IllegalArgumentException when the path is invalid or malformed
+ */
+ public PropertyPath(@NonNull String path) {
+ Objects.requireNonNull(path);
+ mPathList = new ArrayList<>();
+ try {
+ recursivePathScan(path);
+ } catch (IllegalArgumentException e) {
+ // Throw the entire path in a new exception, recursivePathScan may only know about part
+ // of the path.
+ throw new IllegalArgumentException(e.getMessage() + ": " + path);
+ }
+ }
+
+ private void recursivePathScan(String path) throws IllegalArgumentException {
+ // Determine whether the path is just a raw property name with no control characters
+ int controlPos = -1;
+ boolean controlIsIndex = false;
+ for (int i = 0; i < path.length(); i++) {
+ char c = path.charAt(i);
+ if (c == ']') {
+ throw new IllegalArgumentException("Malformed path (no starting '[')");
+ }
+ if (c == '[' || c == '.') {
+ controlPos = i;
+ controlIsIndex = c == '[';
+ break;
+ }
+ }
+
+ if (controlPos == 0 || path.isEmpty()) {
+ throw new IllegalArgumentException("Malformed path (blank property name)");
+ }
+
+ // If the path has no further elements, we're done.
+ if (controlPos == -1) {
+ // The property's cardinality may be REPEATED, but this path isn't indexing into it
+ mPathList.add(new PathSegment(path, PathSegment.NON_REPEATED_CARDINALITY));
+ return;
+ }
+
+ String remainingPath;
+ if (!controlIsIndex) {
+ String propertyName = path.substring(0, controlPos);
+ // Remaining path is everything after the .
+ remainingPath = path.substring(controlPos + 1);
+ mPathList.add(new PathSegment(propertyName, PathSegment.NON_REPEATED_CARDINALITY));
+ } else {
+ remainingPath = consumePropertyWithIndex(path, controlPos);
+ // No more path remains, we have nothing to recurse into
+ if (remainingPath == null) {
+ return;
+ }
+ }
+
+ // More of the path remains; recursively evaluate it
+ recursivePathScan(remainingPath);
+ }
+
+ /**
+ * Helper method to parse the parts of the path String that signify indices with square brackets
+ *
+ * <p>For example, when parsing the path "foo[3]", this will be used to parse the "[3]" part of
+ * the path to determine the index into the preceding "foo" property.
+ *
+ * @param path the string we are parsing
+ * @param controlPos the position of the start bracket
+ * @return the rest of the path after the end brackets, or null if there is nothing after them
+ */
+ @Nullable
+ private String consumePropertyWithIndex(@NonNull String path, int controlPos) {
+ Objects.requireNonNull(path);
+ String propertyName = path.substring(0, controlPos);
+ int endBracketIdx = path.indexOf(']', controlPos);
+ if (endBracketIdx == -1) {
+ throw new IllegalArgumentException("Malformed path (no ending ']')");
+ }
+ if (endBracketIdx + 1 < path.length() && path.charAt(endBracketIdx + 1) != '.') {
+ throw new IllegalArgumentException("Malformed path (']' not followed by '.'): " + path);
+ }
+ String indexStr = path.substring(controlPos + 1, endBracketIdx);
+ int index;
+ try {
+ index = Integer.parseInt(indexStr);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Malformed path (\"" + indexStr + "\" as path index)");
+ }
+ if (index < 0) {
+ throw new IllegalArgumentException("Malformed path (path index less than 0)");
+ }
+ mPathList.add(new PathSegment(propertyName, index));
+ // Remaining path is everything after the [n]
+ if (endBracketIdx + 1 < path.length()) {
+ // More path remains, and we've already checked that charAt(endBracketIdx+1) == .
+ return path.substring(endBracketIdx + 2);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the {@link PathSegment} at a specified index of the PropertyPath.
+ *
+ * <p>Calling {@code get(1)} on a {@link PropertyPath} representing "foo.bar[1]" will return a
+ * {@link PathSegment} representing "bar[1]". {@link PathSegment}s both with and without a
+ * property index of {@link PathSegment#NON_REPEATED_CARDINALITY} are retrieved the same.
+ *
+ * @param index the position into the PropertyPath
+ * @throws ArrayIndexOutOfBoundsException if index is not a valid index in the path list
+ */
+ // Allow use of the Kotlin indexing operator
+ @SuppressWarnings("KotlinOperator")
+ @SuppressLint("KotlinOperator")
+ @NonNull
+ public PathSegment get(int index) {
+ return mPathList.get(index);
+ }
+
+ /**
+ * Returns the number of {@link PathSegment}s in the PropertyPath.
+ *
+ * <p>Paths representing "foo.bar" and "foo[1].bar[1]" will have the same size, as a property
+ * and an index into that property are stored in one {@link PathSegment}.
+ */
+ public int size() {
+ return mPathList.size();
+ }
+
+ /** Returns a valid path string representing this PropertyPath */
+ @Override
+ @NonNull
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < mPathList.size(); i++) {
+ result.append(get(i).toString());
+ if (i < mPathList.size() - 1) {
+ result.append('.');
+ }
+ }
+
+ return result.toString();
+ }
+
+ /** Returns an iterator over the PathSegments within the PropertyPath */
+ @NonNull
+ @Override
+ public Iterator<PathSegment> iterator() {
+ return mPathList.iterator();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null) return false;
+ if (!(o instanceof PropertyPath)) return false;
+ PropertyPath that = (PropertyPath) o;
+ return Objects.equals(mPathList, that.mPathList);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPathList);
+ }
+
+ /**
+ * A segment of a PropertyPath, which includes the name of the property and a 0-based index into
+ * this property.
+ *
+ * <p>If the property index is not set to {@link #NON_REPEATED_CARDINALITY}, this represents a
+ * schema property with the "repeated" cardinality, or a path like "foo[1]". Otherwise, this
+ * represents a schema property that could have any cardinality, or a path like "foo".
+ */
+ public static class PathSegment {
+ /**
+ * A marker variable to signify that a PathSegment represents a schema property that isn't
+ * indexed into. The value is chosen to be invalid if used as an array index.
+ */
+ public static final int NON_REPEATED_CARDINALITY = -1;
+
+ @NonNull private final String mPropertyName;
+ private final int mPropertyIndex;
+
+ /**
+ * Creation method that accepts and validates both a property name and the index into the
+ * property.
+ *
+ * <p>The property name may not be blank. It also may not contain square brackets or dots,
+ * as they are control characters in property paths. The index into the property may not be
+ * negative, unless it is {@link #NON_REPEATED_CARDINALITY}, as these are invalid array
+ * indices.
+ *
+ * @param propertyName the name of the property
+ * @param propertyIndex the index into the property
+ * @return A new PathSegment
+ * @throws IllegalArgumentException if the property name or index is invalid.
+ */
+ @NonNull
+ public static PathSegment create(@NonNull String propertyName, int propertyIndex) {
+ Objects.requireNonNull(propertyName);
+ // A path may contain control characters, but a PathSegment may not
+ if (propertyName.isEmpty()
+ || propertyName.contains("[")
+ || propertyName.contains("]")
+ || propertyName.contains(".")) {
+ throw new IllegalArgumentException("Invalid propertyName value:" + propertyName);
+ }
+ // Has to be a positive integer or the special marker
+ if (propertyIndex < 0 && propertyIndex != NON_REPEATED_CARDINALITY) {
+ throw new IllegalArgumentException("Invalid propertyIndex value:" + propertyIndex);
+ }
+ return new PathSegment(propertyName, propertyIndex);
+ }
+
+ /**
+ * Creation method that accepts and validates a property name
+ *
+ * <p>The property index is set to {@link #NON_REPEATED_CARDINALITY}
+ *
+ * @param propertyName the name of the property
+ * @return A new PathSegment
+ */
+ @NonNull
+ public static PathSegment create(@NonNull String propertyName) {
+ return create(Objects.requireNonNull(propertyName), NON_REPEATED_CARDINALITY);
+ }
+
+ /**
+ * Package-private constructor that accepts a property name and an index into the property
+ * without validating either of them
+ *
+ * @param propertyName the name of the property
+ * @param propertyIndex the index into the property
+ */
+ PathSegment(@NonNull String propertyName, int propertyIndex) {
+ mPropertyName = Objects.requireNonNull(propertyName);
+ mPropertyIndex = propertyIndex;
+ }
+
+ /**
+ * @return the property name
+ */
+ @NonNull
+ public String getPropertyName() {
+ return mPropertyName;
+ }
+
+ /**
+ * Returns the index into the property, or {@link #NON_REPEATED_CARDINALITY} if this does
+ * not represent a PathSegment with an index.
+ */
+ public int getPropertyIndex() {
+ return mPropertyIndex;
+ }
+
+ /** Returns a path representing a PathSegment, either "foo" or "foo[1]" */
+ @Override
+ @NonNull
+ public String toString() {
+ if (mPropertyIndex != NON_REPEATED_CARDINALITY) {
+ return new StringBuilder(mPropertyName)
+ .append("[")
+ .append(mPropertyIndex)
+ .append("]")
+ .toString();
+ }
+ return mPropertyName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null) return false;
+ if (!(o instanceof PathSegment)) return false;
+ PathSegment that = (PathSegment) o;
+ return mPropertyIndex == that.mPropertyIndex
+ && mPropertyName.equals(that.mPropertyName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPropertyName, mPropertyIndex);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/PutDocumentsRequest.java b/android-34/android/app/appsearch/PutDocumentsRequest.java
new file mode 100644
index 0000000..5228b5c
--- /dev/null
+++ b/android-34/android/app/appsearch/PutDocumentsRequest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Encapsulates a request to index documents into an {@link AppSearchSession} database.
+ *
+ * @see AppSearchSession#put
+ */
+public final class PutDocumentsRequest {
+ private final List<GenericDocument> mDocuments;
+
+ PutDocumentsRequest(List<GenericDocument> documents) {
+ mDocuments = documents;
+ }
+
+ /** Returns a list of {@link GenericDocument} objects that are part of this request. */
+ @NonNull
+ public List<GenericDocument> getGenericDocuments() {
+ return Collections.unmodifiableList(mDocuments);
+ }
+
+ /** Builder for {@link PutDocumentsRequest} objects. */
+ public static final class Builder {
+ private ArrayList<GenericDocument> mDocuments = new ArrayList<>();
+ private boolean mBuilt = false;
+
+ /** Adds one or more {@link GenericDocument} objects to the request. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addGenericDocuments(@NonNull GenericDocument... documents) {
+ Objects.requireNonNull(documents);
+ resetIfBuilt();
+ return addGenericDocuments(Arrays.asList(documents));
+ }
+
+ /** Adds a collection of {@link GenericDocument} objects to the request. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addGenericDocuments(
+ @NonNull Collection<? extends GenericDocument> documents) {
+ Objects.requireNonNull(documents);
+ resetIfBuilt();
+ mDocuments.addAll(documents);
+ return this;
+ }
+
+ /** Creates a new {@link PutDocumentsRequest} object. */
+ @NonNull
+ public PutDocumentsRequest build() {
+ mBuilt = true;
+ return new PutDocumentsRequest(mDocuments);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mDocuments = new ArrayList<>(mDocuments);
+ mBuilt = false;
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/RemoveByDocumentIdRequest.java b/android-34/android/app/appsearch/RemoveByDocumentIdRequest.java
new file mode 100644
index 0000000..81a0b3a
--- /dev/null
+++ b/android-34/android/app/appsearch/RemoveByDocumentIdRequest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.util.ArraySet;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Encapsulates a request to remove documents by namespace and IDs from the {@link AppSearchSession}
+ * database.
+ *
+ * @see AppSearchSession#remove
+ */
+public final class RemoveByDocumentIdRequest {
+ private final String mNamespace;
+ private final Set<String> mIds;
+
+ RemoveByDocumentIdRequest(String namespace, Set<String> ids) {
+ mNamespace = namespace;
+ mIds = ids;
+ }
+
+ /** Returns the namespace to remove documents from. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the set of document IDs attached to the request. */
+ @NonNull
+ public Set<String> getIds() {
+ return Collections.unmodifiableSet(mIds);
+ }
+
+ /** Builder for {@link RemoveByDocumentIdRequest} objects. */
+ public static final class Builder {
+ private final String mNamespace;
+ private ArraySet<String> mIds = new ArraySet<>();
+ private boolean mBuilt = false;
+
+ /** Creates a {@link RemoveByDocumentIdRequest.Builder} instance. */
+ public Builder(@NonNull String namespace) {
+ mNamespace = Objects.requireNonNull(namespace);
+ }
+
+ /** Adds one or more document IDs to the request. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addIds(@NonNull String... ids) {
+ Objects.requireNonNull(ids);
+ resetIfBuilt();
+ return addIds(Arrays.asList(ids));
+ }
+
+ /** Adds a collection of IDs to the request. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addIds(@NonNull Collection<String> ids) {
+ Objects.requireNonNull(ids);
+ resetIfBuilt();
+ mIds.addAll(ids);
+ return this;
+ }
+
+ /** Builds a new {@link RemoveByDocumentIdRequest}. */
+ @NonNull
+ public RemoveByDocumentIdRequest build() {
+ mBuilt = true;
+ return new RemoveByDocumentIdRequest(mNamespace, mIds);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mIds = new ArraySet<>(mIds);
+ mBuilt = false;
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/ReportSystemUsageRequest.java b/android-34/android/app/appsearch/ReportSystemUsageRequest.java
new file mode 100644
index 0000000..f175959
--- /dev/null
+++ b/android-34/android/app/appsearch/ReportSystemUsageRequest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+
+import java.util.Objects;
+
+/**
+ * A request to report usage of a document owned by another app from a system UI surface.
+ *
+ * <p>Usage reported in this way is measured separately from usage reported via {@link
+ * AppSearchSession#reportUsage}.
+ *
+ * <p>See {@link GlobalSearchSession#reportSystemUsage} for a detailed description of usage
+ * reporting.
+ */
+public final class ReportSystemUsageRequest {
+ private final String mPackageName;
+ private final String mDatabase;
+ private final String mNamespace;
+ private final String mDocumentId;
+ private final long mUsageTimestampMillis;
+
+ ReportSystemUsageRequest(
+ @NonNull String packageName,
+ @NonNull String database,
+ @NonNull String namespace,
+ @NonNull String documentId,
+ long usageTimestampMillis) {
+ mPackageName = Objects.requireNonNull(packageName);
+ mDatabase = Objects.requireNonNull(database);
+ mNamespace = Objects.requireNonNull(namespace);
+ mDocumentId = Objects.requireNonNull(documentId);
+ mUsageTimestampMillis = usageTimestampMillis;
+ }
+
+ /** Returns the package name of the app which owns the document that was used. */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /** Returns the database in which the document that was used resides. */
+ @NonNull
+ public String getDatabaseName() {
+ return mDatabase;
+ }
+
+ /** Returns the namespace of the document that was used. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the ID of document that was used. */
+ @NonNull
+ public String getDocumentId() {
+ return mDocumentId;
+ }
+
+ /**
+ * Returns the timestamp in milliseconds of the usage report (the time at which the document was
+ * used).
+ *
+ * <p>The value is in the {@link System#currentTimeMillis} time base.
+ */
+ @CurrentTimeMillisLong
+ public long getUsageTimestampMillis() {
+ return mUsageTimestampMillis;
+ }
+
+ /** Builder for {@link ReportSystemUsageRequest} objects. */
+ public static final class Builder {
+ private final String mPackageName;
+ private final String mDatabase;
+ private final String mNamespace;
+ private final String mDocumentId;
+ private Long mUsageTimestampMillis;
+
+ /**
+ * Creates a {@link ReportSystemUsageRequest.Builder} instance.
+ *
+ * @param packageName The package name of the app which owns the document that was used
+ * (e.g. from {@link SearchResult#getPackageName}).
+ * @param databaseName The database in which the document that was used resides (e.g. from
+ * {@link SearchResult#getDatabaseName}).
+ * @param namespace The namespace of the document that was used (e.g. from {@link
+ * GenericDocument#getNamespace}.
+ * @param documentId The ID of document that was used (e.g. from {@link
+ * GenericDocument#getId}.
+ */
+ public Builder(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull String namespace,
+ @NonNull String documentId) {
+ mPackageName = Objects.requireNonNull(packageName);
+ mDatabase = Objects.requireNonNull(databaseName);
+ mNamespace = Objects.requireNonNull(namespace);
+ mDocumentId = Objects.requireNonNull(documentId);
+ }
+
+ /**
+ * Sets the timestamp in milliseconds of the usage report (the time at which the document
+ * was used).
+ *
+ * <p>The value is in the {@link System#currentTimeMillis} time base.
+ *
+ * <p>If unset, this defaults to the current timestamp at the time that the {@link
+ * ReportSystemUsageRequest} is constructed.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public ReportSystemUsageRequest.Builder setUsageTimestampMillis(
+ @CurrentTimeMillisLong long usageTimestampMillis) {
+ mUsageTimestampMillis = usageTimestampMillis;
+ return this;
+ }
+
+ /** Builds a new {@link ReportSystemUsageRequest}. */
+ @NonNull
+ public ReportSystemUsageRequest build() {
+ if (mUsageTimestampMillis == null) {
+ mUsageTimestampMillis = System.currentTimeMillis();
+ }
+ return new ReportSystemUsageRequest(
+ mPackageName, mDatabase, mNamespace, mDocumentId, mUsageTimestampMillis);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/ReportUsageRequest.java b/android-34/android/app/appsearch/ReportUsageRequest.java
new file mode 100644
index 0000000..ac4e6bd
--- /dev/null
+++ b/android-34/android/app/appsearch/ReportUsageRequest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+
+import java.util.Objects;
+
+/**
+ * A request to report usage of a document.
+ *
+ * <p>See {@link AppSearchSession#reportUsage} for a detailed description of usage reporting.
+ *
+ * @see AppSearchSession#reportUsage
+ */
+public final class ReportUsageRequest {
+ private final String mNamespace;
+ private final String mDocumentId;
+ private final long mUsageTimestampMillis;
+
+ ReportUsageRequest(
+ @NonNull String namespace, @NonNull String documentId, long usageTimestampMillis) {
+ mNamespace = Objects.requireNonNull(namespace);
+ mDocumentId = Objects.requireNonNull(documentId);
+ mUsageTimestampMillis = usageTimestampMillis;
+ }
+
+ /** Returns the namespace of the document that was used. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the ID of document that was used. */
+ @NonNull
+ public String getDocumentId() {
+ return mDocumentId;
+ }
+
+ /**
+ * Returns the timestamp in milliseconds of the usage report (the time at which the document was
+ * used).
+ *
+ * <p>The value is in the {@link System#currentTimeMillis} time base.
+ */
+ @CurrentTimeMillisLong
+ public long getUsageTimestampMillis() {
+ return mUsageTimestampMillis;
+ }
+
+ /** Builder for {@link ReportUsageRequest} objects. */
+ public static final class Builder {
+ private final String mNamespace;
+ private final String mDocumentId;
+ private Long mUsageTimestampMillis;
+
+ /**
+ * Creates a new {@link ReportUsageRequest.Builder} instance.
+ *
+ * @param namespace The namespace of the document that was used (e.g. from {@link
+ * GenericDocument#getNamespace}.
+ * @param documentId The ID of document that was used (e.g. from {@link
+ * GenericDocument#getId}.
+ */
+ public Builder(@NonNull String namespace, @NonNull String documentId) {
+ mNamespace = Objects.requireNonNull(namespace);
+ mDocumentId = Objects.requireNonNull(documentId);
+ }
+
+ /**
+ * Sets the timestamp in milliseconds of the usage report (the time at which the document
+ * was used).
+ *
+ * <p>The value is in the {@link System#currentTimeMillis} time base.
+ *
+ * <p>If unset, this defaults to the current timestamp at the time that the {@link
+ * ReportUsageRequest} is constructed.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public ReportUsageRequest.Builder setUsageTimestampMillis(
+ @CurrentTimeMillisLong long usageTimestampMillis) {
+ mUsageTimestampMillis = usageTimestampMillis;
+ return this;
+ }
+
+ /** Builds a new {@link ReportUsageRequest}. */
+ @NonNull
+ public ReportUsageRequest build() {
+ if (mUsageTimestampMillis == null) {
+ mUsageTimestampMillis = System.currentTimeMillis();
+ }
+ return new ReportUsageRequest(mNamespace, mDocumentId, mUsageTimestampMillis);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/SearchResult.java b/android-34/android/app/appsearch/SearchResult.java
new file mode 100644
index 0000000..0390e8c
--- /dev/null
+++ b/android-34/android/app/appsearch/SearchResult.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.os.Bundle;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class represents one of the results obtained from an AppSearch query.
+ *
+ * <p>This allows clients to obtain:
+ *
+ * <ul>
+ * <li>The document which matched, using {@link #getGenericDocument}
+ * <li>Information about which properties in the document matched, and "snippet" information
+ * containing textual summaries of the document's matches, using {@link #getMatchInfos}
+ * </ul>
+ *
+ * <p>"Snippet" refers to a substring of text from the content of document that is returned as a
+ * part of search result.
+ *
+ * @see SearchResults
+ */
+public final class SearchResult {
+ static final String DOCUMENT_FIELD = "document";
+ static final String MATCH_INFOS_FIELD = "matchInfos";
+ static final String PACKAGE_NAME_FIELD = "packageName";
+ static final String DATABASE_NAME_FIELD = "databaseName";
+ static final String RANKING_SIGNAL_FIELD = "rankingSignal";
+ static final String JOINED_RESULTS = "joinedResults";
+
+ @NonNull private final Bundle mBundle;
+
+ /** Cache of the inflated document. Comes from inflating mDocumentBundle at first use. */
+ @Nullable private GenericDocument mDocument;
+
+ /** Cache of the inflated matches. Comes from inflating mMatchBundles at first use. */
+ @Nullable private List<MatchInfo> mMatchInfos;
+
+ /** @hide */
+ public SearchResult(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ /** @hide */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Contains the matching {@link GenericDocument}.
+ *
+ * @return Document object which matched the query.
+ */
+ @NonNull
+ public GenericDocument getGenericDocument() {
+ if (mDocument == null) {
+ mDocument =
+ new GenericDocument(Objects.requireNonNull(mBundle.getBundle(DOCUMENT_FIELD)));
+ }
+ return mDocument;
+ }
+
+ /**
+ * Returns a list of {@link MatchInfo}s providing information about how the document in {@link
+ * #getGenericDocument} matched the query.
+ *
+ * @return List of matches based on {@link SearchSpec}. If snippeting is disabled using {@link
+ * SearchSpec.Builder#setSnippetCount} or {@link
+ * SearchSpec.Builder#setSnippetCountPerProperty}, for all results after that value, this
+ * method returns an empty list.
+ */
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public List<MatchInfo> getMatchInfos() {
+ if (mMatchInfos == null) {
+ List<Bundle> matchBundles =
+ Objects.requireNonNull(mBundle.getParcelableArrayList(MATCH_INFOS_FIELD));
+ mMatchInfos = new ArrayList<>(matchBundles.size());
+ for (int i = 0; i < matchBundles.size(); i++) {
+ MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument());
+ if (mMatchInfos != null) {
+ // This additional check is added for NullnessChecker.
+ mMatchInfos.add(matchInfo);
+ }
+ }
+ }
+ // This check is added for NullnessChecker, mMatchInfos will always be NonNull.
+ return Objects.requireNonNull(mMatchInfos);
+ }
+
+ /**
+ * Contains the package name of the app that stored the {@link GenericDocument}.
+ *
+ * @return Package name that stored the document
+ */
+ @NonNull
+ public String getPackageName() {
+ return Objects.requireNonNull(mBundle.getString(PACKAGE_NAME_FIELD));
+ }
+
+ /**
+ * Contains the database name that stored the {@link GenericDocument}.
+ *
+ * @return Name of the database within which the document is stored
+ */
+ @NonNull
+ public String getDatabaseName() {
+ return Objects.requireNonNull(mBundle.getString(DATABASE_NAME_FIELD));
+ }
+
+ /**
+ * Returns the ranking signal of the {@link GenericDocument}, according to the ranking strategy
+ * set in {@link SearchSpec.Builder#setRankingStrategy(int)}.
+ *
+ * <p>The meaning of the ranking signal and its value is determined by the selected ranking
+ * strategy:
+ *
+ * <ul>
+ * <li>{@link SearchSpec#RANKING_STRATEGY_NONE} - this value will be 0
+ * <li>{@link SearchSpec#RANKING_STRATEGY_DOCUMENT_SCORE} - the value returned by calling
+ * {@link GenericDocument#getScore()} on the document returned by {@link
+ * #getGenericDocument()}
+ * <li>{@link SearchSpec#RANKING_STRATEGY_CREATION_TIMESTAMP} - the value returned by calling
+ * {@link GenericDocument#getCreationTimestampMillis()} on the document returned by {@link
+ * #getGenericDocument()}
+ * <li>{@link SearchSpec#RANKING_STRATEGY_RELEVANCE_SCORE} - an arbitrary double value where a
+ * higher value means more relevant
+ * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} - the number of times usage has been
+ * reported for the document returned by {@link #getGenericDocument()}
+ * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} - the timestamp of the
+ * most recent usage that has been reported for the document returned by {@link
+ * #getGenericDocument()}
+ * </ul>
+ *
+ * @return Ranking signal of the document
+ */
+ public double getRankingSignal() {
+ return mBundle.getDouble(RANKING_SIGNAL_FIELD);
+ }
+
+ /**
+ * Gets a list of {@link SearchResult} joined from the join operation.
+ *
+ * <p>These joined documents match the outer document as specified in the {@link JoinSpec} with
+ * parentPropertyExpression and childPropertyExpression. They are ordered according to the
+ * {@link JoinSpec#getNestedSearchSpec}, and as many SearchResults as specified by {@link
+ * JoinSpec#getMaxJoinedResultCount} will be returned. If no {@link JoinSpec} was specified,
+ * this returns an empty list.
+ *
+ * <p>This method is inefficient to call repeatedly, as new {@link SearchResult} objects are
+ * created each time.
+ *
+ * @return a List of SearchResults containing joined documents.
+ */
+ @NonNull
+ @SuppressWarnings("deprecation") // Bundle#getParcelableArrayList(String) is deprecated.
+ public List<SearchResult> getJoinedResults() {
+ ArrayList<Bundle> bundles = mBundle.getParcelableArrayList(JOINED_RESULTS);
+ if (bundles == null) {
+ return new ArrayList<>();
+ }
+ List<SearchResult> res = new ArrayList<>(bundles.size());
+ for (int i = 0; i < bundles.size(); i++) {
+ res.add(new SearchResult(bundles.get(i)));
+ }
+
+ return res;
+ }
+
+ /** Builder for {@link SearchResult} objects. */
+ public static final class Builder {
+ private final String mPackageName;
+ private final String mDatabaseName;
+ private ArrayList<Bundle> mMatchInfoBundles = new ArrayList<>();
+ private GenericDocument mGenericDocument;
+ private double mRankingSignal;
+ private ArrayList<Bundle> mJoinedResults = new ArrayList<>();
+ private boolean mBuilt = false;
+
+ /**
+ * Constructs a new builder for {@link SearchResult} objects.
+ *
+ * @param packageName the package name the matched document belongs to
+ * @param databaseName the database name the matched document belongs to.
+ */
+ public Builder(@NonNull String packageName, @NonNull String databaseName) {
+ mPackageName = Objects.requireNonNull(packageName);
+ mDatabaseName = Objects.requireNonNull(databaseName);
+ }
+
+ /** Sets the document which matched. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setGenericDocument(@NonNull GenericDocument document) {
+ Objects.requireNonNull(document);
+ resetIfBuilt();
+ mGenericDocument = document;
+ return this;
+ }
+
+ /** Adds another match to this SearchResult. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addMatchInfo(@NonNull MatchInfo matchInfo) {
+ Preconditions.checkState(
+ matchInfo.mDocument == null,
+ "This MatchInfo is already associated with a SearchResult and can't be "
+ + "reassigned");
+ resetIfBuilt();
+ mMatchInfoBundles.add(matchInfo.mBundle);
+ return this;
+ }
+
+ /** Sets the ranking signal of the matched document in this SearchResult. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setRankingSignal(double rankingSignal) {
+ resetIfBuilt();
+ mRankingSignal = rankingSignal;
+ return this;
+ }
+
+ /**
+ * Adds a {@link SearchResult} that was joined by the {@link JoinSpec}.
+ *
+ * @param joinedResult The joined SearchResult to add.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addJoinedResult(@NonNull SearchResult joinedResult) {
+ resetIfBuilt();
+ mJoinedResults.add(joinedResult.getBundle());
+ return this;
+ }
+
+ /** Constructs a new {@link SearchResult}. */
+ @NonNull
+ public SearchResult build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(PACKAGE_NAME_FIELD, mPackageName);
+ bundle.putString(DATABASE_NAME_FIELD, mDatabaseName);
+ bundle.putBundle(DOCUMENT_FIELD, mGenericDocument.getBundle());
+ bundle.putDouble(RANKING_SIGNAL_FIELD, mRankingSignal);
+ bundle.putParcelableArrayList(MATCH_INFOS_FIELD, mMatchInfoBundles);
+ bundle.putParcelableArrayList(JOINED_RESULTS, mJoinedResults);
+ mBuilt = true;
+ return new SearchResult(bundle);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mMatchInfoBundles = new ArrayList<>(mMatchInfoBundles);
+ mJoinedResults = new ArrayList<>(mJoinedResults);
+ mBuilt = false;
+ }
+ }
+ }
+
+ /**
+ * This class represents match objects for any Snippets that might be present in {@link
+ * SearchResults} from a query. Using this class, the user can get:
+ *
+ * <ul>
+ * <li>the full text - all of the text in that String property
+ * <li>the exact term match - the 'term' (full word) that matched the query
+ * <li>the subterm match - the portion of the matched term that appears in the query
+ * <li>a suggested text snippet - a portion of the full text surrounding the exact term match,
+ * set to term boundaries. The size of the snippet is specified in {@link
+ * SearchSpec.Builder#setMaxSnippetSize}
+ * </ul>
+ *
+ * for each match in the document.
+ *
+ * <p>Class Example 1:
+ *
+ * <p>A document contains the following text in property "subject":
+ *
+ * <p>"A commonly used fake word is foo. Another nonsense word that’s used a lot is bar."
+ *
+ * <p>If the queryExpression is "foo" and {@link SearchSpec#getMaxSnippetSize} is 10,
+ *
+ * <ul>
+ * <li>{@link MatchInfo#getPropertyPath()} returns "subject"
+ * <li>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
+ * nonsense word that’s used a lot is bar."
+ * <li>{@link MatchInfo#getExactMatchRange()} returns [29, 32]
+ * <li>{@link MatchInfo#getExactMatch()} returns "foo"
+ * <li>{@link MatchInfo#getSubmatchRange()} returns [29, 32]
+ * <li>{@link MatchInfo#getSubmatch()} returns "foo"
+ * <li>{@link MatchInfo#getSnippetRange()} returns [26, 33]
+ * <li>{@link MatchInfo#getSnippet()} returns "is foo."
+ * </ul>
+ *
+ * <p>
+ *
+ * <p>Class Example 2:
+ *
+ * <p>A document contains one property named "subject" and one property named "sender" which
+ * contains a "name" property.
+ *
+ * <p>In this case, we will have 2 property paths: {@code sender.name} and {@code subject}.
+ *
+ * <p>Let {@code sender.name = "Test Name Jr."} and {@code subject = "Testing 1 2 3"}
+ *
+ * <p>If the queryExpression is "Test" with {@link SearchSpec#TERM_MATCH_PREFIX} and {@link
+ * SearchSpec#getMaxSnippetSize} is 10. We will have 2 matches:
+ *
+ * <p>Match-1
+ *
+ * <ul>
+ * <li>{@link MatchInfo#getPropertyPath()} returns "sender.name"
+ * <li>{@link MatchInfo#getFullText()} returns "Test Name Jr."
+ * <li>{@link MatchInfo#getExactMatchRange()} returns [0, 4]
+ * <li>{@link MatchInfo#getExactMatch()} returns "Test"
+ * <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4]
+ * <li>{@link MatchInfo#getSubmatch()} returns "Test"
+ * <li>{@link MatchInfo#getSnippetRange()} returns [0, 9]
+ * <li>{@link MatchInfo#getSnippet()} returns "Test Name"
+ * </ul>
+ *
+ * <p>Match-2
+ *
+ * <ul>
+ * <li>{@link MatchInfo#getPropertyPath()} returns "subject"
+ * <li>{@link MatchInfo#getFullText()} returns "Testing 1 2 3"
+ * <li>{@link MatchInfo#getExactMatchRange()} returns [0, 7]
+ * <li>{@link MatchInfo#getExactMatch()} returns "Testing"
+ * <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4]
+ * <li>{@link MatchInfo#getSubmatch()} returns "Test"
+ * <li>{@link MatchInfo#getSnippetRange()} returns [0, 9]
+ * <li>{@link MatchInfo#getSnippet()} returns "Testing 1"
+ * </ul>
+ */
+ public static final class MatchInfo {
+ /** The path of the matching snippet property. */
+ private static final String PROPERTY_PATH_FIELD = "propertyPath";
+
+ private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower";
+ private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper";
+ private static final String SUBMATCH_RANGE_LOWER_FIELD = "submatchRangeLower";
+ private static final String SUBMATCH_RANGE_UPPER_FIELD = "submatchRangeUpper";
+ private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower";
+ private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper";
+
+ private final String mPropertyPath;
+ @Nullable private PropertyPath mPropertyPathObject = null;
+ final Bundle mBundle;
+
+ /**
+ * Document which the match comes from.
+ *
+ * <p>If this is {@code null}, methods which require access to the document, like {@link
+ * #getExactMatch}, will throw {@link NullPointerException}.
+ */
+ @Nullable final GenericDocument mDocument;
+
+ /** Full text of the matched property. Populated on first use. */
+ @Nullable private String mFullText;
+
+ /** Range of property that exactly matched the query. Populated on first use. */
+ @Nullable private MatchRange mExactMatchRange;
+
+ /**
+ * Range of property that corresponds to the subsequence of the exact match that directly
+ * matches a query term. Populated on first use.
+ */
+ @Nullable private MatchRange mSubmatchRange;
+
+ /** Range of some reasonable amount of context around the query. Populated on first use. */
+ @Nullable private MatchRange mWindowRange;
+
+ MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) {
+ mBundle = Objects.requireNonNull(bundle);
+ mDocument = document;
+ mPropertyPath = Objects.requireNonNull(bundle.getString(PROPERTY_PATH_FIELD));
+ }
+
+ /**
+ * Gets the property path corresponding to the given entry.
+ *
+ * <p>A property path is a '.' - delimited sequence of property names indicating which
+ * property in the document these snippets correspond to.
+ *
+ * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class
+ * example 1 this returns "subject"
+ */
+ @NonNull
+ public String getPropertyPath() {
+ return mPropertyPath;
+ }
+
+ /**
+ * Gets a {@link PropertyPath} object representing the property path corresponding to the
+ * given entry.
+ *
+ * <p>Methods such as {@link GenericDocument#getPropertyDocument} accept a path as a string
+ * rather than a {@link PropertyPath} object. However, you may want to manipulate the path
+ * before getting a property document. This method returns a {@link PropertyPath} rather
+ * than a String for easier path manipulation, which can then be converted to a String.
+ *
+ * @see #getPropertyPath
+ * @see PropertyPath
+ */
+ @NonNull
+ public PropertyPath getPropertyPathObject() {
+ if (mPropertyPathObject == null) {
+ mPropertyPathObject = new PropertyPath(mPropertyPath);
+ }
+ return mPropertyPathObject;
+ }
+
+ /**
+ * Gets the full text corresponding to the given entry.
+ *
+ * <p>Class example 1: this returns "A commonly used fake word is foo. Another nonsense word
+ * that's used a lot is bar."
+ *
+ * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name Jr." and,
+ * for the second {@link MatchInfo}, this returns "Testing 1 2 3".
+ */
+ @NonNull
+ public String getFullText() {
+ if (mFullText == null) {
+ if (mDocument == null) {
+ throw new IllegalStateException(
+ "Document has not been populated; this MatchInfo cannot be used yet");
+ }
+ mFullText = getPropertyValues(mDocument, mPropertyPath);
+ }
+ return mFullText;
+ }
+
+ /**
+ * Gets the {@link MatchRange} of the exact term of the given entry that matched the query.
+ *
+ * <p>Class example 1: this returns [29, 32].
+ *
+ * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 4] and, for the
+ * second {@link MatchInfo}, this returns [0, 7].
+ */
+ @NonNull
+ public MatchRange getExactMatchRange() {
+ if (mExactMatchRange == null) {
+ mExactMatchRange =
+ new MatchRange(
+ mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD),
+ mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD));
+ }
+ return mExactMatchRange;
+ }
+
+ /**
+ * Gets the exact term of the given entry that matched the query.
+ *
+ * <p>Class example 1: this returns "foo".
+ *
+ * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test" and, for the
+ * second {@link MatchInfo}, this returns "Testing".
+ */
+ @NonNull
+ public CharSequence getExactMatch() {
+ return getSubstring(getExactMatchRange());
+ }
+
+ /**
+ * Gets the {@link MatchRange} of the exact term subsequence of the given entry that matched
+ * the query.
+ *
+ * <p>Class example 1: this returns [29, 32].
+ *
+ * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 4] and, for the
+ * second {@link MatchInfo}, this returns [0, 4].
+ */
+ @NonNull
+ public MatchRange getSubmatchRange() {
+ checkSubmatchSupported();
+ if (mSubmatchRange == null) {
+ mSubmatchRange =
+ new MatchRange(
+ mBundle.getInt(SUBMATCH_RANGE_LOWER_FIELD),
+ mBundle.getInt(SUBMATCH_RANGE_UPPER_FIELD));
+ }
+ return mSubmatchRange;
+ }
+
+ /**
+ * Gets the exact term subsequence of the given entry that matched the query.
+ *
+ * <p>Class example 1: this returns "foo".
+ *
+ * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test" and, for the
+ * second {@link MatchInfo}, this returns "Test".
+ */
+ @NonNull
+ public CharSequence getSubmatch() {
+ checkSubmatchSupported();
+ return getSubstring(getSubmatchRange());
+ }
+
+ /**
+ * Gets the snippet {@link MatchRange} corresponding to the given entry.
+ *
+ * <p>Only populated when set maxSnippetSize > 0 in {@link
+ * SearchSpec.Builder#setMaxSnippetSize}.
+ *
+ * <p>Class example 1: this returns [29, 41].
+ *
+ * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 9] and, for the
+ * second {@link MatchInfo}, this returns [0, 13].
+ */
+ @NonNull
+ public MatchRange getSnippetRange() {
+ if (mWindowRange == null) {
+ mWindowRange =
+ new MatchRange(
+ mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD),
+ mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD));
+ }
+ return mWindowRange;
+ }
+
+ /**
+ * Gets the snippet corresponding to the given entry.
+ *
+ * <p>Snippet - Provides a subset of the content to display. Only populated when requested
+ * maxSnippetSize > 0. The size of this content can be changed by {@link
+ * SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of the
+ * matched token with content on either side clipped to token boundaries.
+ *
+ * <p>Class example 1: this returns "foo. Another".
+ *
+ * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name" and, for
+ * the second {@link MatchInfo}, this returns "Testing 1 2 3".
+ */
+ @NonNull
+ public CharSequence getSnippet() {
+ return getSubstring(getSnippetRange());
+ }
+
+ private CharSequence getSubstring(MatchRange range) {
+ return getFullText().substring(range.getStart(), range.getEnd());
+ }
+
+ private void checkSubmatchSupported() {
+ if (!mBundle.containsKey(SUBMATCH_RANGE_LOWER_FIELD)) {
+ throw new UnsupportedOperationException(
+ "Submatch is not supported with this backend/Android API level "
+ + "combination");
+ }
+ }
+
+ /** Extracts the matching string from the document. */
+ private static String getPropertyValues(GenericDocument document, String propertyName) {
+ String result = document.getPropertyString(propertyName);
+ if (result == null) {
+ throw new IllegalStateException(
+ "No content found for requested property path: " + propertyName);
+ }
+ return result;
+ }
+
+ /** Builder for {@link MatchInfo} objects. */
+ public static final class Builder {
+ private final String mPropertyPath;
+ private MatchRange mExactMatchRange = new MatchRange(0, 0);
+ @Nullable private MatchRange mSubmatchRange;
+ private MatchRange mSnippetRange = new MatchRange(0, 0);
+
+ /**
+ * Creates a new {@link MatchInfo.Builder} reporting a match with the given property
+ * path.
+ *
+ * <p>A property path is a dot-delimited sequence of property names indicating which
+ * property in the document these snippets correspond to.
+ *
+ * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class
+ * example 1 this returns "subject".
+ *
+ * @param propertyPath A dot-delimited sequence of property names indicating which
+ * property in the document these snippets correspond to.
+ */
+ public Builder(@NonNull String propertyPath) {
+ mPropertyPath = Objects.requireNonNull(propertyPath);
+ }
+
+ /** Sets the exact {@link MatchRange} corresponding to the given entry. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setExactMatchRange(@NonNull MatchRange matchRange) {
+ mExactMatchRange = Objects.requireNonNull(matchRange);
+ return this;
+ }
+
+ /** Sets the submatch {@link MatchRange} corresponding to the given entry. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setSubmatchRange(@NonNull MatchRange matchRange) {
+ mSubmatchRange = Objects.requireNonNull(matchRange);
+ return this;
+ }
+
+ /** Sets the snippet {@link MatchRange} corresponding to the given entry. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setSnippetRange(@NonNull MatchRange matchRange) {
+ mSnippetRange = Objects.requireNonNull(matchRange);
+ return this;
+ }
+
+ /** Constructs a new {@link MatchInfo}. */
+ @NonNull
+ public MatchInfo build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, mPropertyPath);
+ bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, mExactMatchRange.getStart());
+ bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, mExactMatchRange.getEnd());
+ if (mSubmatchRange != null) {
+ // Only populate the submatch fields if it was actually set.
+ bundle.putInt(MatchInfo.SUBMATCH_RANGE_LOWER_FIELD, mSubmatchRange.getStart());
+ }
+
+ if (mSubmatchRange != null) {
+ // Only populate the submatch fields if it was actually set.
+ // Moved to separate block for Nullness Checker.
+ bundle.putInt(MatchInfo.SUBMATCH_RANGE_UPPER_FIELD, mSubmatchRange.getEnd());
+ }
+
+ bundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, mSnippetRange.getStart());
+ bundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, mSnippetRange.getEnd());
+ return new MatchInfo(bundle, /*document=*/ null);
+ }
+ }
+ }
+
+ /**
+ * Class providing the position range of matching information.
+ *
+ * <p>All ranges are finite, and the left side of the range is always {@code <=} the right side
+ * of the range.
+ *
+ * <p>Example: MatchRange(0, 100) represent a hundred ints from 0 to 99."
+ */
+ public static final class MatchRange {
+ private final int mEnd;
+ private final int mStart;
+
+ /**
+ * Creates a new immutable range.
+ *
+ * <p>The endpoints are {@code [start, end)}; that is the range is bounded. {@code start}
+ * must be lesser or equal to {@code end}.
+ *
+ * @param start The start point (inclusive)
+ * @param end The end point (exclusive)
+ */
+ public MatchRange(int start, int end) {
+ if (start > end) {
+ throw new IllegalArgumentException(
+ "Start point must be less than or equal to " + "end point");
+ }
+ mStart = start;
+ mEnd = end;
+ }
+
+ /** Gets the start point (inclusive). */
+ public int getStart() {
+ return mStart;
+ }
+
+ /** Gets the end point (exclusive). */
+ public int getEnd() {
+ return mEnd;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MatchRange)) {
+ return false;
+ }
+ MatchRange otherMatchRange = (MatchRange) other;
+ return this.getStart() == otherMatchRange.getStart()
+ && this.getEnd() == otherMatchRange.getEnd();
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "MatchRange { start: " + mStart + " , end: " + mEnd + "}";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mStart, mEnd);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/SearchResultPage.java b/android-34/android/app/appsearch/SearchResultPage.java
new file mode 100644
index 0000000..c04dcda
--- /dev/null
+++ b/android-34/android/app/appsearch/SearchResultPage.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class represents a page of {@link SearchResult}s
+ *
+ * @hide
+ */
+public class SearchResultPage {
+ public static final String RESULTS_FIELD = "results";
+ public static final String NEXT_PAGE_TOKEN_FIELD = "nextPageToken";
+ private final long mNextPageToken;
+
+ @Nullable private List<SearchResult> mResults;
+
+ @NonNull private final Bundle mBundle;
+
+ public SearchResultPage(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ mNextPageToken = mBundle.getLong(NEXT_PAGE_TOKEN_FIELD);
+ }
+
+ /** Returns the {@link Bundle} of this class. */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /** Returns the Token to get next {@link SearchResultPage}. */
+ public long getNextPageToken() {
+ return mNextPageToken;
+ }
+
+ /** Returns all {@link android.app.appsearch.SearchResult}s of this page */
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public List<SearchResult> getResults() {
+ if (mResults == null) {
+ ArrayList<Bundle> resultBundles = mBundle.getParcelableArrayList(RESULTS_FIELD);
+ if (resultBundles == null) {
+ mResults = Collections.emptyList();
+ } else {
+ mResults = new ArrayList<>(resultBundles.size());
+ for (int i = 0; i < resultBundles.size(); i++) {
+ mResults.add(new SearchResult(resultBundles.get(i)));
+ }
+ }
+ }
+ return mResults;
+ }
+}
diff --git a/android-34/android/app/appsearch/SearchResults.java b/android-34/android/app/appsearch/SearchResults.java
new file mode 100644
index 0000000..1191df3
--- /dev/null
+++ b/android-34/android/app/appsearch/SearchResults.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import static android.app.appsearch.AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
+import static android.app.appsearch.AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID;
+import static android.app.appsearch.SearchSessionUtil.safeExecute;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.IAppSearchManager;
+import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.content.AttributionSource;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.Closeable;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Encapsulates results of a search operation.
+ *
+ * <p>Each {@link AppSearchSession#search} operation returns a list of {@link SearchResult} objects,
+ * referred to as a "page", limited by the size configured by {@link
+ * SearchSpec.Builder#setResultCountPerPage}.
+ *
+ * <p>To fetch a page of results, call {@link #getNextPage}.
+ *
+ * <p>All instances of {@link SearchResults} must call {@link SearchResults#close()} after the
+ * results are fetched.
+ *
+ * <p>This class is not thread safe.
+ */
+public class SearchResults implements Closeable {
+ private static final String TAG = "SearchResults";
+
+ private final IAppSearchManager mService;
+
+ // The permission identity of the caller
+ private final AttributionSource mAttributionSource;
+
+ // The database name to search over. If null, this will search over all database names.
+ @Nullable
+ private final String mDatabaseName;
+
+ private final String mQueryExpression;
+
+ private final SearchSpec mSearchSpec;
+
+ private final UserHandle mUserHandle;
+
+ private long mNextPageToken;
+
+ private boolean mIsFirstLoad = true;
+
+ private boolean mIsClosed = false;
+
+ SearchResults(
+ @NonNull IAppSearchManager service,
+ @NonNull AttributionSource attributionSource,
+ @Nullable String databaseName,
+ @NonNull String queryExpression,
+ @NonNull SearchSpec searchSpec,
+ @NonNull UserHandle userHandle) {
+ mService = Objects.requireNonNull(service);
+ mAttributionSource = Objects.requireNonNull(attributionSource);
+ mDatabaseName = databaseName;
+ mQueryExpression = Objects.requireNonNull(queryExpression);
+ mSearchSpec = Objects.requireNonNull(searchSpec);
+ mUserHandle = Objects.requireNonNull(userHandle);
+ }
+
+ /**
+ * Retrieves the next page of {@link SearchResult} objects.
+ *
+ * <p>The page size is configured by {@link SearchSpec.Builder#setResultCountPerPage}.
+ *
+ * <p>Continue calling this method to access results until it returns an empty list, signifying
+ * there are no more results.
+ *
+ * @param executor Executor on which to invoke the callback.
+ * @param callback Callback to receive the pending result of performing this operation.
+ */
+ public void getNextPage(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
+ try {
+ long binderCallStartTimeMillis = SystemClock.elapsedRealtime();
+ if (mIsFirstLoad) {
+ mIsFirstLoad = false;
+ if (mDatabaseName == null) {
+ // Global query, there's no one package-database combination to check.
+ mService.globalQuery(mAttributionSource, mQueryExpression,
+ mSearchSpec.getBundle(), mUserHandle, binderCallStartTimeMillis,
+ wrapCallback(executor, callback));
+ } else {
+ // Normal local query, pass in specified database.
+ mService.query(mAttributionSource, mDatabaseName, mQueryExpression,
+ mSearchSpec.getBundle(), mUserHandle,
+ binderCallStartTimeMillis,
+ wrapCallback(executor, callback));
+ }
+ } else {
+ // TODO(b/276349029): Log different join types when they get added.
+ @AppSearchSchema.StringPropertyConfig.JoinableValueType
+ int joinType = JOINABLE_VALUE_TYPE_NONE;
+ if (mSearchSpec.getJoinSpec() != null
+ && !mSearchSpec.getJoinSpec().getChildPropertyExpression().isEmpty()) {
+ joinType = JOINABLE_VALUE_TYPE_QUALIFIED_ID;
+ }
+ mService.getNextPage(mAttributionSource, mDatabaseName, mNextPageToken, joinType,
+ mUserHandle, binderCallStartTimeMillis, wrapCallback(executor, callback));
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void close() {
+ if (!mIsClosed) {
+ try {
+ mService.invalidateNextPageToken(mAttributionSource, mNextPageToken,
+ mUserHandle, /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime());
+ mIsClosed = true;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to close the SearchResults", e);
+ }
+ }
+ }
+
+ private IAppSearchResultCallback wrapCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback) {
+ return new IAppSearchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchResultParcel resultParcel) {
+ safeExecute(
+ executor,
+ callback,
+ () -> invokeCallback(resultParcel.getResult(), callback));
+ }
+ };
+ }
+
+ private void invokeCallback(
+ @NonNull AppSearchResult<Bundle> searchResultPageResult,
+ @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback) {
+ if (searchResultPageResult.isSuccess()) {
+ try {
+ SearchResultPage searchResultPage = new SearchResultPage
+ (Objects.requireNonNull(searchResultPageResult.getResultValue()));
+ mNextPageToken = searchResultPage.getNextPageToken();
+ callback.accept(AppSearchResult.newSuccessfulResult(
+ searchResultPage.getResults()));
+ } catch (Throwable t) {
+ callback.accept(AppSearchResult.throwableToFailedResult(t));
+ }
+ } else {
+ callback.accept(AppSearchResult.newFailedResult(searchResultPageResult));
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/SearchSessionUtil.java b/android-34/android/app/appsearch/SearchSessionUtil.java
new file mode 100644
index 0000000..e6273bd
--- /dev/null
+++ b/android-34/android/app/appsearch/SearchSessionUtil.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.app.appsearch.aidl.AppSearchBatchResultParcel;
+import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * @hide
+ * Contains util methods used in both {@link GlobalSearchSession} and {@link AppSearchSession}.
+ */
+public class SearchSessionUtil {
+ private static final String TAG = "AppSearchSessionUtil";
+
+ /**
+ * Constructor for in case we create an instance
+ */
+ private SearchSessionUtil() {}
+
+ /**
+ * Calls {@link BatchResultCallback#onSystemError} with a throwable derived from the given
+ * failed {@link AppSearchResult}.
+ *
+ * <p>The {@link AppSearchResult} generally comes from
+ * {@link IAppSearchBatchResultCallback#onSystemError}.
+ *
+ * <p>This method should be called from the callback executor thread.
+ *
+ * @param failedResult the error
+ * @param callback the callback to send the error to
+ */
+ public static void sendSystemErrorToCallback(
+ @NonNull AppSearchResult<?> failedResult, @NonNull BatchResultCallback<?, ?> callback) {
+ Preconditions.checkArgument(!failedResult.isSuccess());
+ Throwable throwable = new AppSearchException(
+ failedResult.getResultCode(), failedResult.getErrorMessage());
+ callback.onSystemError(throwable);
+ }
+
+ /**
+ * Safely executes the given lambda on the given executor.
+ *
+ * <p>The {@link Executor#execute} call is wrapped in a try/catch. This prevents situations like
+ * the executor being shut down or the lambda throwing an exception on a direct executor from
+ * crashing the app.
+ *
+ * <p>If execution fails for the above reasons, a failure notification is delivered to
+ * errorCallback synchronously on the calling thread.
+ *
+ * @param executor The executor on which to safely execute the lambda
+ * @param errorCallback The callback to trigger with a failed {@link AppSearchResult} if
+ * the {@link Executor#execute} call fails.
+ * @param runnable The lambda to execute on the executor
+ */
+ public static <T> void safeExecute(
+ @NonNull Executor executor,
+ @NonNull Consumer<AppSearchResult<T>> errorCallback,
+ @NonNull Runnable runnable) {
+ try {
+ executor.execute(runnable);
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to schedule runnable", t);
+ errorCallback.accept(AppSearchResult.throwableToFailedResult(t));
+ }
+ }
+
+ /**
+ * Safely executes the given lambda on the given executor.
+ *
+ * <p>The {@link Executor#execute} call is wrapped in a try/catch. This prevents situations like
+ * the executor being shut down or the lambda throwing an exception on a direct executor from
+ * crashing the app.
+ *
+ * <p>If execution fails for the above reasons, a failure notification is delivered to
+ * errorCallback synchronously on the calling thread.
+ *
+ * @param executor The executor on which to safely execute the lambda
+ * @param errorCallback The callback to trigger with a failed {@link AppSearchResult} if
+ * the {@link Executor#execute} call fails.
+ * @param runnable The lambda to execute on the executor
+ */
+ public static void safeExecute(
+ @NonNull Executor executor,
+ @NonNull BatchResultCallback<?, ?> errorCallback,
+ @NonNull Runnable runnable) {
+ try {
+ executor.execute(runnable);
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to schedule runnable", t);
+ errorCallback.onSystemError(t);
+ }
+ }
+
+ /**
+ * Handler for asynchronous getDocuments method
+ *
+ * @param executor executor to run the callback
+ * @param callback the next method that uses the {@link GenericDocument}
+ * @return A callback to be executed once an {@link AppSearchBatchResultParcel} is received
+ */
+ public static IAppSearchBatchResultCallback createGetDocumentCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BatchResultCallback<String, GenericDocument> callback) {
+ return new IAppSearchBatchResultCallback.Stub() {
+ @Override
+ public void onResult(AppSearchBatchResultParcel resultParcel) {
+ safeExecute(executor, callback, () -> {
+ AppSearchBatchResult<String, Bundle> result =
+ resultParcel.getResult();
+ AppSearchBatchResult.Builder<String, GenericDocument>
+ documentResultBuilder =
+ new AppSearchBatchResult.Builder<>();
+
+ for (Map.Entry<String, Bundle> bundleEntry :
+ result.getSuccesses().entrySet()) {
+ GenericDocument document;
+ try {
+ document = new GenericDocument(bundleEntry.getValue());
+ } catch (Throwable t) {
+ documentResultBuilder.setFailure(
+ bundleEntry.getKey(),
+ AppSearchResult.RESULT_INTERNAL_ERROR,
+ t.getMessage());
+ continue;
+ }
+ documentResultBuilder.setSuccess(
+ bundleEntry.getKey(), document);
+ }
+
+ for (Map.Entry<String, AppSearchResult<Bundle>> bundleEntry :
+ ((Map<String, AppSearchResult<Bundle>>)
+ result.getFailures()).entrySet()) {
+ documentResultBuilder.setFailure(
+ bundleEntry.getKey(),
+ bundleEntry.getValue().getResultCode(),
+ bundleEntry.getValue().getErrorMessage());
+ }
+ callback.onResult(documentResultBuilder.build());
+
+ });
+ }
+
+ @Override
+ public void onSystemError(AppSearchResultParcel result) {
+ safeExecute(
+ executor, callback,
+ () -> sendSystemErrorToCallback(result.getResult(), callback));
+ }
+ };
+ }
+}
diff --git a/android-34/android/app/appsearch/SearchSpec.java b/android-34/android/app/appsearch/SearchSpec.java
new file mode 100644
index 0000000..b721ff9
--- /dev/null
+++ b/android-34/android/app/appsearch/SearchSpec.java
@@ -0,0 +1,1238 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.util.BundleUtil;
+import android.os.Bundle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class represents the specification logic for AppSearch. It can be used to set the type of
+ * search, like prefix or exact only or apply filters to search for a specific schema type only etc.
+ */
+public final class SearchSpec {
+ /**
+ * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply property paths to
+ * all results, excepting any types that have had their own, specific property paths set.
+ */
+ public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+
+ static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
+ static final String SCHEMA_FIELD = "schema";
+ static final String NAMESPACE_FIELD = "namespace";
+ static final String PACKAGE_NAME_FIELD = "packageName";
+ static final String NUM_PER_PAGE_FIELD = "numPerPage";
+ static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
+ static final String ORDER_FIELD = "order";
+ static final String SNIPPET_COUNT_FIELD = "snippetCount";
+ static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";
+ static final String MAX_SNIPPET_FIELD = "maxSnippet";
+ static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
+ static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
+ static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";
+ static final String TYPE_PROPERTY_WEIGHTS_FIELD = "typePropertyWeightsField";
+ static final String JOIN_SPEC = "joinSpec";
+ static final String ADVANCED_RANKING_EXPRESSION = "advancedRankingExpression";
+ static final String ENABLED_FEATURES_FIELD = "enabledFeatures";
+
+ /** @hide */
+ public static final int DEFAULT_NUM_PER_PAGE = 10;
+
+ // TODO(b/170371356): In framework, we may want these limits to be flag controlled.
+ // If that happens, the @IntRange() directives in this class may have to change.
+ private static final int MAX_NUM_PER_PAGE = 10_000;
+ private static final int MAX_SNIPPET_COUNT = 10_000;
+ private static final int MAX_SNIPPET_PER_PROPERTY_COUNT = 10_000;
+ private static final int MAX_SNIPPET_SIZE_LIMIT = 10_000;
+
+ /**
+ * Term Match Type for the query.
+ *
+ * @hide
+ */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link com.google.android.icing.proto.SearchSpecProto.termMatchType}
+ @IntDef(value = {TERM_MATCH_EXACT_ONLY, TERM_MATCH_PREFIX})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TermMatch {}
+
+ /**
+ * Query terms will only match exact tokens in the index.
+ *
+ * <p>Ex. A query term "foo" will only match indexed token "foo", and not "foot" or "football".
+ */
+ public static final int TERM_MATCH_EXACT_ONLY = 1;
+ /**
+ * Query terms will match indexed tokens when the query term is a prefix of the token.
+ *
+ * <p>Ex. A query term "foo" will match indexed tokens like "foo", "foot", and "football".
+ */
+ public static final int TERM_MATCH_PREFIX = 2;
+
+ /**
+ * Ranking Strategy for query result.
+ *
+ * @hide
+ */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link ScoringSpecProto.RankingStrategy.Code}
+ @IntDef(
+ value = {
+ RANKING_STRATEGY_NONE,
+ RANKING_STRATEGY_DOCUMENT_SCORE,
+ RANKING_STRATEGY_CREATION_TIMESTAMP,
+ RANKING_STRATEGY_RELEVANCE_SCORE,
+ RANKING_STRATEGY_USAGE_COUNT,
+ RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP,
+ RANKING_STRATEGY_SYSTEM_USAGE_COUNT,
+ RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP,
+ RANKING_STRATEGY_JOIN_AGGREGATE_SCORE,
+ RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RankingStrategy {}
+
+ /** No Ranking, results are returned in arbitrary order. */
+ public static final int RANKING_STRATEGY_NONE = 0;
+ /** Ranked by app-provided document scores. */
+ public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1;
+ /** Ranked by document creation timestamps. */
+ public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2;
+ /** Ranked by document relevance score. */
+ public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3;
+ /** Ranked by number of usages, as reported by the app. */
+ public static final int RANKING_STRATEGY_USAGE_COUNT = 4;
+ /** Ranked by timestamp of last usage, as reported by the app. */
+ public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5;
+ /** Ranked by number of usages from a system UI surface. */
+ public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6;
+ /** Ranked by timestamp of last usage from a system UI surface. */
+ public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7;
+ /**
+ * Ranked by the aggregated ranking signal of the joined documents.
+ *
+ * <p>Which aggregation strategy is used to determine a ranking signal is specified in the
+ * {@link JoinSpec} set by {@link Builder#setJoinSpec}. This ranking strategy may not be used if
+ * no {@link JoinSpec} is provided.
+ *
+ * @see Builder#build
+ */
+ public static final int RANKING_STRATEGY_JOIN_AGGREGATE_SCORE = 8;
+ /** Ranked by the advanced ranking expression provided. */
+ public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9;
+
+ /**
+ * Order for query result.
+ *
+ * @hide
+ */
+ // NOTE: The integer values of these constants must match the proto enum constants in
+ // {@link ScoringSpecProto.Order.Code}
+ @IntDef(value = {ORDER_DESCENDING, ORDER_ASCENDING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Order {}
+
+ /** Search results will be returned in a descending order. */
+ public static final int ORDER_DESCENDING = 0;
+ /** Search results will be returned in an ascending order. */
+ public static final int ORDER_ASCENDING = 1;
+
+ /**
+ * Grouping type for result limits.
+ *
+ * @hide
+ */
+ @IntDef(
+ flag = true,
+ value = {
+ GROUPING_TYPE_PER_PACKAGE,
+ GROUPING_TYPE_PER_NAMESPACE,
+ GROUPING_TYPE_PER_SCHEMA
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface GroupingType {}
+ /**
+ * Results should be grouped together by package for the purpose of enforcing a limit on the
+ * number of results returned per package.
+ */
+ public static final int GROUPING_TYPE_PER_PACKAGE = 1 << 0;
+ /**
+ * Results should be grouped together by namespace for the purpose of enforcing a limit on the
+ * number of results returned per namespace.
+ */
+ public static final int GROUPING_TYPE_PER_NAMESPACE = 1 << 1;
+ /**
+ * Results should be grouped together by schema type for the purpose of enforcing a limit on the
+ * number of results returned per schema type.
+ * @hide
+ */
+ public static final int GROUPING_TYPE_PER_SCHEMA = 1 << 2;
+
+ private final Bundle mBundle;
+
+ /** @hide */
+ public SearchSpec(@NonNull Bundle bundle) {
+ Objects.requireNonNull(bundle);
+ mBundle = bundle;
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /** Returns how the query terms should match terms in the index. */
+ @TermMatch
+ public int getTermMatch() {
+ return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1);
+ }
+
+ /**
+ * Returns the list of schema types to search for.
+ *
+ * <p>If empty, the query will search over all schema types.
+ */
+ @NonNull
+ public List<String> getFilterSchemas() {
+ List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD);
+ if (schemas == null) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(schemas);
+ }
+
+ /**
+ * Returns the list of namespaces to search over.
+ *
+ * <p>If empty, the query will search over all namespaces.
+ */
+ @NonNull
+ public List<String> getFilterNamespaces() {
+ List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
+ if (namespaces == null) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(namespaces);
+ }
+
+ /**
+ * Returns the list of package name filters to search over.
+ *
+ * <p>If empty, the query will search over all packages that the caller has access to. If
+ * package names are specified which caller doesn't have access to, then those package names
+ * will be ignored.
+ */
+ @NonNull
+ public List<String> getFilterPackageNames() {
+ List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD);
+ if (packageNames == null) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(packageNames);
+ }
+
+ /** Returns the number of results per page in the result set. */
+ public int getResultCountPerPage() {
+ return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
+ }
+
+ /** Returns the ranking strategy. */
+ @RankingStrategy
+ public int getRankingStrategy() {
+ return mBundle.getInt(RANKING_STRATEGY_FIELD);
+ }
+
+ /** Returns the order of returned search results (descending or ascending). */
+ @Order
+ public int getOrder() {
+ return mBundle.getInt(ORDER_FIELD);
+ }
+
+ /** Returns how many documents to generate snippets for. */
+ public int getSnippetCount() {
+ return mBundle.getInt(SNIPPET_COUNT_FIELD);
+ }
+
+ /**
+ * Returns how many matches for each property of a matching document to generate snippets for.
+ */
+ public int getSnippetCountPerProperty() {
+ return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD);
+ }
+
+ /** Returns the maximum size of a snippet in characters. */
+ public int getMaxSnippetSize() {
+ return mBundle.getInt(MAX_SNIPPET_FIELD);
+ }
+
+ /**
+ * Returns a map from schema type to property paths to be used for projection.
+ *
+ * <p>If the map is empty, then all properties will be retrieved for all results.
+ *
+ * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this
+ * function, rather than calling it multiple times.
+ *
+ * @return A mapping of schema types to lists of projection strings.
+ */
+ @NonNull
+ public Map<String, List<String>> getProjections() {
+ Bundle typePropertyPathsBundle =
+ Objects.requireNonNull(mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD));
+ Set<String> schemas = typePropertyPathsBundle.keySet();
+ Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+ for (String schema : schemas) {
+ typePropertyPathsMap.put(
+ schema,
+ Objects.requireNonNull(typePropertyPathsBundle.getStringArrayList(schema)));
+ }
+ return typePropertyPathsMap;
+ }
+
+ /**
+ * Returns a map from schema type to property paths to be used for projection.
+ *
+ * <p>If the map is empty, then all properties will be retrieved for all results.
+ *
+ * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this
+ * function, rather than calling it multiple times.
+ *
+ * @return A mapping of schema types to lists of projection {@link PropertyPath} objects.
+ */
+ @NonNull
+ public Map<String, List<PropertyPath>> getProjectionPaths() {
+ Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
+ Set<String> schemas = typePropertyPathsBundle.keySet();
+ Map<String, List<PropertyPath>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+ for (String schema : schemas) {
+ ArrayList<String> propertyPathList = typePropertyPathsBundle.getStringArrayList(schema);
+ List<PropertyPath> copy = new ArrayList<>(propertyPathList.size());
+ for (String p : propertyPathList) {
+ copy.add(new PropertyPath(p));
+ }
+ typePropertyPathsMap.put(schema, copy);
+ }
+ return typePropertyPathsMap;
+ }
+
+ /**
+ * Returns properties weights to be used for scoring.
+ *
+ * <p>Calling this function repeatedly is inefficient. Prefer to retain the {@link Map} returned
+ * by this function, rather than calling it multiple times.
+ *
+ * @return a {@link Map} of schema type to an inner-map of property paths of the schema type to
+ * the weight to set for that property.
+ */
+ @NonNull
+ public Map<String, Map<String, Double>> getPropertyWeights() {
+ Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);
+ Set<String> schemaTypes = typePropertyWeightsBundle.keySet();
+ Map<String, Map<String, Double>> typePropertyWeightsMap =
+ new ArrayMap<>(schemaTypes.size());
+ for (String schemaType : schemaTypes) {
+ Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);
+ Set<String> propertyPaths = propertyPathBundle.keySet();
+ Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
+ for (String propertyPath : propertyPaths) {
+ propertyPathWeights.put(propertyPath, propertyPathBundle.getDouble(propertyPath));
+ }
+ typePropertyWeightsMap.put(schemaType, propertyPathWeights);
+ }
+ return typePropertyWeightsMap;
+ }
+
+ /**
+ * Returns properties weights to be used for scoring.
+ *
+ * <p>Calling this function repeatedly is inefficient. Prefer to retain the {@link Map} returned
+ * by this function, rather than calling it multiple times.
+ *
+ * @return a {@link Map} of schema type to an inner-map of property paths of the schema type to
+ * the weight to set for that property.
+ */
+ @NonNull
+ public Map<String, Map<PropertyPath, Double>> getPropertyWeightPaths() {
+ Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);
+ Set<String> schemaTypes = typePropertyWeightsBundle.keySet();
+ Map<String, Map<PropertyPath, Double>> typePropertyWeightsMap =
+ new ArrayMap<>(schemaTypes.size());
+ for (String schemaType : schemaTypes) {
+ Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);
+ Set<String> propertyPaths = propertyPathBundle.keySet();
+ Map<PropertyPath, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
+ for (String propertyPath : propertyPaths) {
+ propertyPathWeights.put(
+ new PropertyPath(propertyPath), propertyPathBundle.getDouble(propertyPath));
+ }
+ typePropertyWeightsMap.put(schemaType, propertyPathWeights);
+ }
+ return typePropertyWeightsMap;
+ }
+
+ /**
+ * Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not
+ * called.
+ */
+ @GroupingType
+ public int getResultGroupingTypeFlags() {
+ return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);
+ }
+
+ /**
+ * Get the maximum number of results to return for each group.
+ *
+ * @return the maximum number of results to return for each group or Integer.MAX_VALUE if {@link
+ * Builder#setResultGrouping(int, int)} was not called.
+ */
+ public int getResultGroupingLimit() {
+ return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE);
+ }
+
+ /** Returns specification on which documents need to be joined. */
+ @Nullable
+ public JoinSpec getJoinSpec() {
+ Bundle joinSpec = mBundle.getBundle(JOIN_SPEC);
+ if (joinSpec == null) {
+ return null;
+ }
+ return new JoinSpec(joinSpec);
+ }
+
+ /**
+ * Get the advanced ranking expression, or "" if {@link Builder#setRankingStrategy(String)} was
+ * not called.
+ */
+ @NonNull
+ public String getAdvancedRankingExpression() {
+ return mBundle.getString(ADVANCED_RANKING_EXPRESSION, "");
+ }
+
+ /** Returns whether the {@link Features#NUMERIC_SEARCH} feature is enabled. */
+ public boolean isNumericSearchEnabled() {
+ return getEnabledFeatures().contains(FeatureConstants.NUMERIC_SEARCH);
+ }
+
+ /** Returns whether the {@link Features#VERBATIM_SEARCH} feature is enabled. */
+ public boolean isVerbatimSearchEnabled() {
+ return getEnabledFeatures().contains(FeatureConstants.VERBATIM_SEARCH);
+ }
+
+ /** Returns whether the {@link Features#LIST_FILTER_QUERY_LANGUAGE} feature is enabled. */
+ public boolean isListFilterQueryLanguageEnabled() {
+ return getEnabledFeatures().contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE);
+ }
+
+ /**
+ * Get the list of enabled features that the caller is intending to use in this search call.
+ *
+ * @return the set of {@link Features} enabled in this {@link SearchSpec} Entry.
+ * @hide
+ */
+ @NonNull
+ public List<String> getEnabledFeatures() {
+ return mBundle.getStringArrayList(ENABLED_FEATURES_FIELD);
+ }
+
+ /** Builder for {@link SearchSpec objects}. */
+ public static final class Builder {
+ private ArrayList<String> mSchemas = new ArrayList<>();
+ private ArrayList<String> mNamespaces = new ArrayList<>();
+ private ArrayList<String> mPackageNames = new ArrayList<>();
+ private ArraySet<String> mEnabledFeatures = new ArraySet<>();
+ private Bundle mProjectionTypePropertyMasks = new Bundle();
+ private Bundle mTypePropertyWeights = new Bundle();
+
+ private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
+ @TermMatch private int mTermMatchType = TERM_MATCH_PREFIX;
+ private int mSnippetCount = 0;
+ private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT;
+ private int mMaxSnippetSize = 0;
+ @RankingStrategy private int mRankingStrategy = RANKING_STRATEGY_NONE;
+ @Order private int mOrder = ORDER_DESCENDING;
+ @GroupingType private int mGroupingTypeFlags = 0;
+ private int mGroupingLimit = 0;
+ private JoinSpec mJoinSpec;
+ private String mAdvancedRankingExpression = "";
+ private boolean mBuilt = false;
+
+ /**
+ * Indicates how the query terms should match {@code TermMatchCode} in the index.
+ *
+ * <p>If this method is not called, the default term match type is {@link
+ * SearchSpec#TERM_MATCH_PREFIX}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTermMatch(@TermMatch int termMatchType) {
+ Preconditions.checkArgumentInRange(
+ termMatchType, TERM_MATCH_EXACT_ONLY, TERM_MATCH_PREFIX, "Term match type");
+ resetIfBuilt();
+ mTermMatchType = termMatchType;
+ return this;
+ }
+
+ /**
+ * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that
+ * have the specified schema types.
+ *
+ * <p>If unset, the query will search over all schema types.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterSchemas(@NonNull String... schemas) {
+ Objects.requireNonNull(schemas);
+ resetIfBuilt();
+ return addFilterSchemas(Arrays.asList(schemas));
+ }
+
+ /**
+ * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that
+ * have the specified schema types.
+ *
+ * <p>If unset, the query will search over all schema types.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterSchemas(@NonNull Collection<String> schemas) {
+ Objects.requireNonNull(schemas);
+ resetIfBuilt();
+ mSchemas.addAll(schemas);
+ return this;
+ }
+
+ /**
+ * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that have
+ * the specified namespaces.
+ *
+ * <p>If unset, the query will search over all namespaces.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterNamespaces(@NonNull String... namespaces) {
+ Objects.requireNonNull(namespaces);
+ resetIfBuilt();
+ return addFilterNamespaces(Arrays.asList(namespaces));
+ }
+
+ /**
+ * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that have
+ * the specified namespaces.
+ *
+ * <p>If unset, the query will search over all namespaces.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) {
+ Objects.requireNonNull(namespaces);
+ resetIfBuilt();
+ mNamespaces.addAll(namespaces);
+ return this;
+ }
+
+ /**
+ * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
+ * were indexed from the specified packages.
+ *
+ * <p>If unset, the query will search over all packages that the caller has access to. If
+ * package names are specified which caller doesn't have access to, then those package names
+ * will be ignored.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterPackageNames(@NonNull String... packageNames) {
+ Objects.requireNonNull(packageNames);
+ resetIfBuilt();
+ return addFilterPackageNames(Arrays.asList(packageNames));
+ }
+
+ /**
+ * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
+ * were indexed from the specified packages.
+ *
+ * <p>If unset, the query will search over all packages that the caller has access to. If
+ * package names are specified which caller doesn't have access to, then those package names
+ * will be ignored.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterPackageNames(@NonNull Collection<String> packageNames) {
+ Objects.requireNonNull(packageNames);
+ resetIfBuilt();
+ mPackageNames.addAll(packageNames);
+ return this;
+ }
+
+ /**
+ * Sets the number of results per page in the returned object.
+ *
+ * <p>The default number of results per page is 10.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public SearchSpec.Builder setResultCountPerPage(
+ @IntRange(from = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage) {
+ Preconditions.checkArgumentInRange(
+ resultCountPerPage, 0, MAX_NUM_PER_PAGE, "resultCountPerPage");
+ resetIfBuilt();
+ mResultCountPerPage = resultCountPerPage;
+ return this;
+ }
+
+ /** Sets ranking strategy for AppSearch results. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) {
+ Preconditions.checkArgumentInRange(
+ rankingStrategy,
+ RANKING_STRATEGY_NONE,
+ RANKING_STRATEGY_JOIN_AGGREGATE_SCORE,
+ "Result ranking strategy");
+ resetIfBuilt();
+ mRankingStrategy = rankingStrategy;
+ mAdvancedRankingExpression = "";
+ return this;
+ }
+
+ /**
+ * Enables advanced ranking to score based on {@code advancedRankingExpression}.
+ *
+ * <p>This method will set RankingStrategy to {@link
+ * #RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION}.
+ *
+ * <p>The ranking expression is a mathematical expression that will be evaluated to a
+ * floating-point number of double type representing the score of each document.
+ *
+ * <p>Numeric literals, arithmetic operators, mathematical functions, and document-based
+ * functions are supported to build expressions.
+ *
+ * <p>The following are supported arithmetic operators:
+ *
+ * <ul>
+ * <li>Addition(+)
+ * <li>Subtraction(-)
+ * <li>Multiplication(*)
+ * <li>Floating Point Division(/)
+ * </ul>
+ *
+ * <p>Operator precedences are compliant with the Java Language, and parentheses are
+ * supported. For example, "2.2 + (3 - 4) / 2" evaluates to 1.7.
+ *
+ * <p>The following are supported basic mathematical functions:
+ *
+ * <ul>
+ * <li>log(x) - the natural log of x
+ * <li>log(x, y) - the log of y with base x
+ * <li>pow(x, y) - x to the power of y
+ * <li>sqrt(x)
+ * <li>abs(x)
+ * <li>sin(x), cos(x), tan(x)
+ * <li>Example: "max(abs(-100), 10) + pow(2, 10)" will be evaluated to 1124
+ * </ul>
+ *
+ * <p>The following variadic mathematical functions are supported, with n > 0. They also
+ * accept list value parameters. For example, if V is a value of list type, we can call
+ * sum(V) to get the sum of all the values in V. List literals are not supported, so a value
+ * of list type can only be constructed as a return value of some particular document-based
+ * functions.
+ *
+ * <ul>
+ * <li>max(v1, v2, ..., vn) or max(V)
+ * <li>min(v1, v2, ..., vn) or min(V)
+ * <li>len(v1, v2, ..., vn) or len(V)
+ * <li>sum(v1, v2, ..., vn) or sum(V)
+ * <li>avg(v1, v2, ..., vn) or avg(V)
+ * </ul>
+ *
+ * <p>Document-based functions must be called via "this", which represents the current
+ * document being scored. The following are supported document-based functions:
+ *
+ * <ul>
+ * <li>this.documentScore()
+ * <p>Get the app-provided document score of the current document. This is the same
+ * score that is returned for {@link #RANKING_STRATEGY_DOCUMENT_SCORE}.
+ * <li>this.creationTimestamp()
+ * <p>Get the creation timestamp of the current document. This is the same score that
+ * is returned for {@link #RANKING_STRATEGY_CREATION_TIMESTAMP}.
+ * <li>this.relevanceScore()
+ * <p>Get the BM25F relevance score of the current document in relation to the query
+ * string. This is the same score that is returned for {@link
+ * #RANKING_STRATEGY_RELEVANCE_SCORE}.
+ * <li>this.usageCount(type) and this.usageLastUsedTimestamp(type)
+ * <p>Get the number of usages or the timestamp of last usage by type for the current
+ * document, where type must be evaluated to an integer from 1 to 2. Type 1 refers to
+ * usages reported by {@link AppSearchSession#reportUsage}, and type 2 refers to
+ * usages reported by {@link GlobalSearchSession#reportSystemUsage}.
+ * <li>this.childrenScores()
+ * <p>Returns a list of children document scores. Currently, a document can only be a
+ * child of another document in the context of joins. If this function is called
+ * without the Join API enabled, a type error will be raised.
+ * <li>this.propertyWeights()
+ * <p>Returns a list of the normalized weights of the matched properties for the
+ * current document being scored. Property weights come from what's specified in
+ * {@link SearchSpec}. After normalizing, each provided weight will be divided by the
+ * maximum weight, so that each of them will be <= 1.
+ * </ul>
+ *
+ * <p>Some errors may occur when using advanced ranking.
+ *
+ * <p>Syntax Error: the expression violates the syntax of the advanced ranking language.
+ * Below are some examples.
+ *
+ * <ul>
+ * <li>"1 + " - missing operand
+ * <li>"2 * (1 + 2))" - unbalanced parenthesis
+ * <li>"2 ^ 3" - unknown operator
+ * </ul>
+ *
+ * <p>Type Error: the expression fails a static type check. Below are some examples.
+ *
+ * <ul>
+ * <li>"sin(2, 3)" - wrong number of arguments for the sin function
+ * <li>"this.childrenScores() + 1" - cannot add a list with a number
+ * <li>"this.propertyWeights()" - the final type of the overall expression cannot be a
+ * list, which can be fixed by "max(this.propertyWeights())"
+ * <li>"abs(this.propertyWeights())" - the abs function does not support list type
+ * arguments
+ * <li>"print(2)" - unknown function
+ * </ul>
+ *
+ * <p>Evaluation Error: an error occurred while evaluating the value of the expression.
+ * Below are some examples.
+ *
+ * <ul>
+ * <li>"1 / 0", "log(0)", "1 + sqrt(-1)" - getting a non-finite value in the middle of
+ * evaluation
+ * <li>"this.usageCount(1 + 0.5)" - expect the argument to be an integer. Note that this
+ * is not a type error and "this.usageCount(1.5 + 1/2)" can succeed without any issues
+ * <li>"this.documentScore()" - in case of an IO error, this will be an evaluation error
+ * </ul>
+ *
+ * <p>Syntax errors and type errors will fail the entire search and will cause {@link
+ * SearchResults#getNextPage} to throw an {@link AppSearchException} with the result code of
+ * {@link AppSearchResult#RESULT_INVALID_ARGUMENT}.
+ *
+ * <p>Evaluation errors will result in the offending documents receiving the default score.
+ * For {@link #ORDER_DESCENDING}, the default score will be 0, for {@link #ORDER_ASCENDING}
+ * the default score will be infinity.
+ *
+ * @param advancedRankingExpression a non-empty string representing the ranking expression.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setRankingStrategy(@NonNull String advancedRankingExpression) {
+ Preconditions.checkStringNotEmpty(advancedRankingExpression);
+ resetIfBuilt();
+ mRankingStrategy = RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION;
+ mAdvancedRankingExpression = advancedRankingExpression;
+ return this;
+ }
+
+ /**
+ * Indicates the order of returned search results, the default is {@link #ORDER_DESCENDING},
+ * meaning that results with higher scores come first.
+ *
+ * <p>This order field will be ignored if RankingStrategy = {@code RANKING_STRATEGY_NONE}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setOrder(@Order int order) {
+ Preconditions.checkArgumentInRange(
+ order, ORDER_DESCENDING, ORDER_ASCENDING, "Result ranking order");
+ resetIfBuilt();
+ mOrder = order;
+ return this;
+ }
+
+ /**
+ * Only the first {@code snippetCount} documents based on the ranking strategy will have
+ * snippet information provided.
+ *
+ * <p>The list returned from {@link SearchResult#getMatchInfos} will contain at most this
+ * many entries.
+ *
+ * <p>If set to 0 (default), snippeting is disabled and the list returned from {@link
+ * SearchResult#getMatchInfos} will be empty.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public SearchSpec.Builder setSnippetCount(
+ @IntRange(from = 0, to = MAX_SNIPPET_COUNT) int snippetCount) {
+ Preconditions.checkArgumentInRange(snippetCount, 0, MAX_SNIPPET_COUNT, "snippetCount");
+ resetIfBuilt();
+ mSnippetCount = snippetCount;
+ return this;
+ }
+
+ /**
+ * Sets {@code snippetCountPerProperty}. Only the first {@code snippetCountPerProperty}
+ * snippets for each property of each {@link GenericDocument} will contain snippet
+ * information.
+ *
+ * <p>If set to 0, snippeting is disabled and the list returned from {@link
+ * SearchResult#getMatchInfos} will be empty.
+ *
+ * <p>The default behavior is to snippet all matches a property contains, up to the maximum
+ * value of 10,000.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public SearchSpec.Builder setSnippetCountPerProperty(
+ @IntRange(from = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT)
+ int snippetCountPerProperty) {
+ Preconditions.checkArgumentInRange(
+ snippetCountPerProperty,
+ 0,
+ MAX_SNIPPET_PER_PROPERTY_COUNT,
+ "snippetCountPerProperty");
+ resetIfBuilt();
+ mSnippetCountPerProperty = snippetCountPerProperty;
+ return this;
+ }
+
+ /**
+ * Sets {@code maxSnippetSize}, the maximum snippet size. Snippet windows start at {@code
+ * maxSnippetSize/2} bytes before the middle of the matching token and end at {@code
+ * maxSnippetSize/2} bytes after the middle of the matching token. It respects token
+ * boundaries, therefore the returned window may be smaller than requested.
+ *
+ * <p>Setting {@code maxSnippetSize} to 0 will disable windowing and an empty string will be
+ * returned. If matches enabled is also set to false, then snippeting is disabled.
+ *
+ * <p>Ex. {@code maxSnippetSize} = 16. "foo bar baz bat rat" with a query of "baz" will
+ * return a window of "bar baz bat" which is only 11 bytes long.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public SearchSpec.Builder setMaxSnippetSize(
+ @IntRange(from = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize) {
+ Preconditions.checkArgumentInRange(
+ maxSnippetSize, 0, MAX_SNIPPET_SIZE_LIMIT, "maxSnippetSize");
+ resetIfBuilt();
+ mMaxSnippetSize = maxSnippetSize;
+ return this;
+ }
+
+ /**
+ * Adds property paths for the specified type to be used for projection. If property paths
+ * are added for a type, then only the properties referred to will be retrieved for results
+ * of that type. If a property path that is specified isn't present in a result, it will be
+ * ignored for that result. Property paths cannot be null.
+ *
+ * @see #addProjectionPaths
+ * @param schema a string corresponding to the schema to add projections to.
+ * @param propertyPaths the projections to add.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public SearchSpec.Builder addProjection(
+ @NonNull String schema, @NonNull Collection<String> propertyPaths) {
+ Objects.requireNonNull(schema);
+ Objects.requireNonNull(propertyPaths);
+ resetIfBuilt();
+ ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
+ for (String propertyPath : propertyPaths) {
+ Objects.requireNonNull(propertyPath);
+ propertyPathsArrayList.add(propertyPath);
+ }
+ mProjectionTypePropertyMasks.putStringArrayList(schema, propertyPathsArrayList);
+ return this;
+ }
+
+ /**
+ * Adds property paths for the specified type to be used for projection. If property paths
+ * are added for a type, then only the properties referred to will be retrieved for results
+ * of that type. If a property path that is specified isn't present in a result, it will be
+ * ignored for that result. Property paths cannot be null.
+ *
+ * <p>If no property paths are added for a particular type, then all properties of results
+ * of that type will be retrieved.
+ *
+ * <p>If property path is added for the {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD},
+ * then those property paths will apply to all results, excepting any types that have their
+ * own, specific property paths set.
+ *
+ * <p>Suppose the following document is in the index.
+ *
+ * <pre>{@code
+ * Email: Document {
+ * sender: Document {
+ * name: "Mr. Person"
+ * email: "mrperson123@google.com"
+ * }
+ * recipients: [
+ * Document {
+ * name: "John Doe"
+ * email: "johndoe123@google.com"
+ * }
+ * Document {
+ * name: "Jane Doe"
+ * email: "janedoe123@google.com"
+ * }
+ * ]
+ * subject: "IMPORTANT"
+ * body: "Limited time offer!"
+ * }
+ * }</pre>
+ *
+ * <p>Then, suppose that a query for "important" is issued with the following projection
+ * type property paths:
+ *
+ * <pre>{@code
+ * {schema: "Email", ["subject", "sender.name", "recipients.name"]}
+ * }</pre>
+ *
+ * <p>The above document will be returned as:
+ *
+ * <pre>{@code
+ * Email: Document {
+ * sender: Document {
+ * name: "Mr. Body"
+ * }
+ * recipients: [
+ * Document {
+ * name: "John Doe"
+ * }
+ * Document {
+ * name: "Jane Doe"
+ * }
+ * ]
+ * subject: "IMPORTANT"
+ * }
+ * }</pre>
+ *
+ * @param schema a string corresponding to the schema to add projections to.
+ * @param propertyPaths the projections to add.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public SearchSpec.Builder addProjectionPaths(
+ @NonNull String schema, @NonNull Collection<PropertyPath> propertyPaths) {
+ Objects.requireNonNull(schema);
+ Objects.requireNonNull(propertyPaths);
+ ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
+ for (PropertyPath propertyPath : propertyPaths) {
+ propertyPathsArrayList.add(propertyPath.toString());
+ }
+ return addProjection(schema, propertyPathsArrayList);
+ }
+
+ /**
+ * Sets the maximum number of results to return for each group, where groups are defined by
+ * grouping type.
+ *
+ * <p>Calling this method will override any previous calls. So calling
+ * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 7) and then calling
+ * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 2) will result in only the latter, a limit
+ * of two results per package, being applied. Or calling setResultGrouping
+ * (GROUPING_TYPE_PER_PACKAGE, 1) and then calling setResultGrouping
+ * (GROUPING_TYPE_PER_PACKAGE | GROUPING_PER_NAMESPACE, 5) will result in five results per
+ * package per namespace.
+ *
+ * @param groupingTypeFlags One or more combination of grouping types.
+ * @param limit Number of results to return per {@code groupingTypeFlags}.
+ * @throws IllegalArgumentException if groupingTypeFlags is zero.
+ */
+ // Individual parameters available from getResultGroupingTypeFlags and
+ // getResultGroupingLimit
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setResultGrouping(@GroupingType int groupingTypeFlags, int limit) {
+ Preconditions.checkState(
+ groupingTypeFlags != 0, "Result grouping type cannot be zero.");
+ resetIfBuilt();
+ mGroupingTypeFlags = groupingTypeFlags;
+ mGroupingLimit = limit;
+ return this;
+ }
+
+ /**
+ * Sets property weights by schema type and property path.
+ *
+ * <p>Property weights are used to promote and demote query term matches within a {@link
+ * GenericDocument} property when applying scoring.
+ *
+ * <p>Property weights must be positive values (greater than 0). A property's weight is
+ * multiplied with that property's scoring contribution. This means weights set between 0.0
+ * and 1.0 demote scoring contributions by a term match within the property. Weights set
+ * above 1.0 promote scoring contributions by a term match within the property.
+ *
+ * <p>Properties that exist in the {@link AppSearchSchema}, but do not have a weight
+ * explicitly set will be given a default weight of 1.0.
+ *
+ * <p>Weights set for property paths that do not exist in the {@link AppSearchSchema} will
+ * be discarded and not affect scoring.
+ *
+ * <p><b>NOTE:</b> Property weights only affect scoring for query-dependent scoring
+ * strategies, such as {@link #RANKING_STRATEGY_RELEVANCE_SCORE}.
+ *
+ * @param schemaType the schema type to set property weights for.
+ * @param propertyPathWeights a {@link Map} of property paths of the schema type to the
+ * weight to set for that property.
+ * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
+ */
+ @NonNull
+ public SearchSpec.Builder setPropertyWeights(
+ @NonNull String schemaType, @NonNull Map<String, Double> propertyPathWeights) {
+ Objects.requireNonNull(schemaType);
+ Objects.requireNonNull(propertyPathWeights);
+
+ Bundle propertyPathBundle = new Bundle();
+ for (Map.Entry<String, Double> propertyPathWeightEntry :
+ propertyPathWeights.entrySet()) {
+ String propertyPath = Objects.requireNonNull(propertyPathWeightEntry.getKey());
+ Double weight = Objects.requireNonNull(propertyPathWeightEntry.getValue());
+ if (weight <= 0.0) {
+ throw new IllegalArgumentException(
+ "Cannot set non-positive property weight "
+ + "value "
+ + weight
+ + " for property path: "
+ + propertyPath);
+ }
+ propertyPathBundle.putDouble(propertyPath, weight);
+ }
+ mTypePropertyWeights.putBundle(schemaType, propertyPathBundle);
+ return this;
+ }
+
+ /**
+ * Specifies which documents to join with, and how to join.
+ *
+ * <p>If the ranking strategy is {@link #RANKING_STRATEGY_JOIN_AGGREGATE_SCORE}, and the
+ * JoinSpec is null, {@link #build} will throw an {@link AppSearchException}.
+ *
+ * @param joinSpec a specification on how to perform the Join operation.
+ */
+ @NonNull
+ public Builder setJoinSpec(@NonNull JoinSpec joinSpec) {
+ resetIfBuilt();
+ mJoinSpec = Objects.requireNonNull(joinSpec);
+ return this;
+ }
+
+ /**
+ * Sets property weights by schema type and property path.
+ *
+ * <p>Property weights are used to promote and demote query term matches within a {@link
+ * GenericDocument} property when applying scoring.
+ *
+ * <p>Property weights must be positive values (greater than 0). A property's weight is
+ * multiplied with that property's scoring contribution. This means weights set between 0.0
+ * and 1.0 demote scoring contributions by a term match within the property. Weights set
+ * above 1.0 promote scoring contributions by a term match within the property.
+ *
+ * <p>Properties that exist in the {@link AppSearchSchema}, but do not have a weight
+ * explicitly set will be given a default weight of 1.0.
+ *
+ * <p>Weights set for property paths that do not exist in the {@link AppSearchSchema} will
+ * be discarded and not affect scoring.
+ *
+ * <p><b>NOTE:</b> Property weights only affect scoring for query-dependent scoring
+ * strategies, such as {@link #RANKING_STRATEGY_RELEVANCE_SCORE}.
+ *
+ * @param schemaType the schema type to set property weights for.
+ * @param propertyPathWeights a {@link Map} of property paths of the schema type to the
+ * weight to set for that property.
+ * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
+ */
+ @NonNull
+ public SearchSpec.Builder setPropertyWeightPaths(
+ @NonNull String schemaType,
+ @NonNull Map<PropertyPath, Double> propertyPathWeights) {
+ Objects.requireNonNull(propertyPathWeights);
+
+ Map<String, Double> propertyWeights = new ArrayMap<>(propertyPathWeights.size());
+ for (Map.Entry<PropertyPath, Double> propertyPathWeightEntry :
+ propertyPathWeights.entrySet()) {
+ PropertyPath propertyPath =
+ Objects.requireNonNull(propertyPathWeightEntry.getKey());
+ propertyWeights.put(propertyPath.toString(), propertyPathWeightEntry.getValue());
+ }
+ return setPropertyWeights(schemaType, propertyWeights);
+ }
+
+ /**
+ * Sets the {@link Features#NUMERIC_SEARCH} feature as enabled/disabled according to the
+ * enabled parameter.
+ *
+ * @param enabled Enables the feature if true, otherwise disables it.
+ * <p>If disabled, disallows use of {@link
+ * AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric
+ * querying features.
+ */
+ @NonNull
+ public Builder setNumericSearchEnabled(boolean enabled) {
+ modifyEnabledFeature(FeatureConstants.NUMERIC_SEARCH, enabled);
+ return this;
+ }
+
+ /**
+ * Sets the {@link Features#VERBATIM_SEARCH} feature as enabled/disabled according to the
+ * enabled parameter.
+ *
+ * @param enabled Enables the feature if true, otherwise disables it
+ * <p>If disabled, disallows use of {@link
+ * AppSearchSchema.StringPropertyConfig#TOKENIZER_TYPE_VERBATIM} and all other verbatim
+ * search features within the query language that allows clients to search using the
+ * verbatim string operator.
+ * <p>Ex. The verbatim string operator '"foo/bar" OR baz' will ensure that 'foo/bar' is
+ * treated as a single 'verbatim' token.
+ */
+ @NonNull
+ public Builder setVerbatimSearchEnabled(boolean enabled) {
+ modifyEnabledFeature(FeatureConstants.VERBATIM_SEARCH, enabled);
+ return this;
+ }
+
+ /**
+ * Sets the {@link Features#LIST_FILTER_QUERY_LANGUAGE} feature as enabled/disabled
+ * according to the enabled parameter.
+ *
+ * @param enabled Enables the feature if true, otherwise disables it.
+ * <p>This feature covers the expansion of the query language to conform to the
+ * definition of the list filters language (https://aip.dev/160). This includes:
+ * <ul>
+ * <li>addition of explicit 'AND' and 'NOT' operators
+ * <li>property restricts are allowed with grouping (ex. "prop:(a OR b)")
+ * <li>addition of custom functions to control matching
+ * </ul>
+ * <p>The newly added custom functions covered by this feature are:
+ * <ul>
+ * <li>createList(String...)
+ * <li>termSearch(String, List<String>)
+ * </ul>
+ * <p>createList takes a variable number of strings and returns a list of strings. It is
+ * for use with termSearch.
+ * <p>termSearch takes a query string that will be parsed according to the supported
+ * query language and an optional list of strings that specify the properties to be
+ * restricted to. This exists as a convenience for multiple property restricts. So, for
+ * example, the query "(subject:foo OR body:foo) (subject:bar OR body:bar)" could be
+ * rewritten as "termSearch(\"foo bar\", createList(\"subject\", \"bar\"))"
+ */
+ @NonNull
+ public Builder setListFilterQueryLanguageEnabled(boolean enabled) {
+ modifyEnabledFeature(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE, enabled);
+ return this;
+ }
+
+ /**
+ * Constructs a new {@link SearchSpec} from the contents of this builder.
+ *
+ * @throws IllegalArgumentException if property weights are provided with a ranking strategy
+ * that isn't RANKING_STRATEGY_RELEVANCE_SCORE.
+ * @throws IllegalStateException if the ranking strategy is {@link
+ * #RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} and {@link #setJoinSpec} has never been
+ * called.
+ * @throws IllegalStateException if the aggregation scoring strategy has been set in {@link
+ * JoinSpec#getAggregationScoringStrategy()} but the ranking strategy is not {@link
+ * #RANKING_STRATEGY_JOIN_AGGREGATE_SCORE}.
+ */
+ @NonNull
+ public SearchSpec build() {
+ Bundle bundle = new Bundle();
+ if (mJoinSpec != null) {
+ if (mRankingStrategy != RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
+ && mJoinSpec.getAggregationScoringStrategy()
+ != JoinSpec.AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL) {
+ throw new IllegalStateException(
+ "Aggregate scoring strategy has been set in "
+ + "the nested JoinSpec, but ranking strategy is not "
+ + "RANKING_STRATEGY_JOIN_AGGREGATE_SCORE");
+ }
+ bundle.putBundle(JOIN_SPEC, mJoinSpec.getBundle());
+ } else if (mRankingStrategy == RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) {
+ throw new IllegalStateException(
+ "Attempting to rank based on joined documents, but "
+ + "no JoinSpec provided");
+ }
+ bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
+ bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
+ bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);
+ bundle.putStringArrayList(ENABLED_FEATURES_FIELD, new ArrayList<>(mEnabledFeatures));
+ bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);
+ bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage);
+ bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType);
+ bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount);
+ bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty);
+ bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize);
+ bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
+ bundle.putInt(ORDER_FIELD, mOrder);
+ bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);
+ bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);
+ if (!mTypePropertyWeights.isEmpty()
+ && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy
+ && RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION != mRankingStrategy) {
+ throw new IllegalArgumentException(
+ "Property weights are only compatible with the"
+ + " RANKING_STRATEGY_RELEVANCE_SCORE and"
+ + " RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION ranking strategies.");
+ }
+ bundle.putBundle(TYPE_PROPERTY_WEIGHTS_FIELD, mTypePropertyWeights);
+ bundle.putString(ADVANCED_RANKING_EXPRESSION, mAdvancedRankingExpression);
+ mBuilt = true;
+ return new SearchSpec(bundle);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mSchemas = new ArrayList<>(mSchemas);
+ mNamespaces = new ArrayList<>(mNamespaces);
+ mPackageNames = new ArrayList<>(mPackageNames);
+ mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
+ mTypePropertyWeights = BundleUtil.deepCopy(mTypePropertyWeights);
+ mBuilt = false;
+ }
+ }
+
+ private void modifyEnabledFeature(@NonNull String feature, boolean enabled) {
+ resetIfBuilt();
+ if (enabled) {
+ mEnabledFeatures.add(feature);
+ } else {
+ mEnabledFeatures.remove(feature);
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/SearchSuggestionResult.java b/android-34/android/app/appsearch/SearchSuggestionResult.java
new file mode 100644
index 0000000..fab8770
--- /dev/null
+++ b/android-34/android/app/appsearch/SearchSuggestionResult.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.util.BundleUtil;
+import android.os.Bundle;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/** The result class of the {@link AppSearchSession#searchSuggestion}. */
+public final class SearchSuggestionResult {
+
+ private static final String SUGGESTED_RESULT_FIELD = "suggestedResult";
+ private final Bundle mBundle;
+ @Nullable private Integer mHashCode;
+
+ SearchSuggestionResult(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Returns the suggested result that could be used as query expression in the {@link
+ * AppSearchSession#search}.
+ *
+ * <p>The suggested result will never be empty.
+ *
+ * <p>The suggested result only contains lowercase or special characters.
+ */
+ @NonNull
+ public String getSuggestedResult() {
+ return Objects.requireNonNull(mBundle.getString(SUGGESTED_RESULT_FIELD));
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SearchSuggestionResult)) {
+ return false;
+ }
+ SearchSuggestionResult otherResult = (SearchSuggestionResult) other;
+ return BundleUtil.deepEquals(this.mBundle, otherResult.mBundle);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == null) {
+ mHashCode = BundleUtil.deepHashCode(mBundle);
+ }
+ return mHashCode;
+ }
+
+ /** The Builder class of {@link SearchSuggestionResult}. */
+ public static final class Builder {
+ private String mSuggestedResult = "";
+
+ /**
+ * Sets the suggested result that could be used as query expression in the {@link
+ * AppSearchSession#search}.
+ *
+ * <p>The suggested result should only contain lowercase or special characters.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setSuggestedResult(@NonNull String suggestedResult) {
+ Objects.requireNonNull(suggestedResult);
+ Preconditions.checkStringNotEmpty(suggestedResult);
+ mSuggestedResult = suggestedResult;
+ return this;
+ }
+
+ /** Build a {@link SearchSuggestionResult} object */
+ @NonNull
+ public SearchSuggestionResult build() {
+ Bundle bundle = new Bundle();
+ bundle.putString(SUGGESTED_RESULT_FIELD, mSuggestedResult);
+ return new SearchSuggestionResult(bundle);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/SearchSuggestionSpec.java b/android-34/android/app/appsearch/SearchSuggestionSpec.java
new file mode 100644
index 0000000..cc24236
--- /dev/null
+++ b/android-34/android/app/appsearch/SearchSuggestionSpec.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.util.BundleUtil;
+import android.os.Bundle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class represents the specification logic for AppSearch. It can be used to set the filter and
+ * settings of search a suggestions.
+ *
+ * @see AppSearchSession#searchSuggestion
+ */
+public final class SearchSuggestionSpec {
+ static final String NAMESPACE_FIELD = "namespace";
+ static final String SCHEMA_FIELD = "schema";
+ static final String PROPERTY_FIELD = "property";
+ static final String DOCUMENT_IDS_FIELD = "documentIds";
+ static final String MAXIMUM_RESULT_COUNT_FIELD = "maximumResultCount";
+ static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
+ private final Bundle mBundle;
+ private final int mMaximumResultCount;
+
+ /** @hide */
+ public SearchSuggestionSpec(@NonNull Bundle bundle) {
+ Objects.requireNonNull(bundle);
+ mBundle = bundle;
+ mMaximumResultCount = bundle.getInt(MAXIMUM_RESULT_COUNT_FIELD);
+ Preconditions.checkArgument(
+ mMaximumResultCount >= 1, "MaximumResultCount must be positive.");
+ }
+
+ /**
+ * Ranking Strategy for {@link SearchSuggestionResult}.
+ *
+ * @hide
+ */
+ @IntDef(
+ value = {
+ SUGGESTION_RANKING_STRATEGY_NONE,
+ SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT,
+ SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SuggestionRankingStrategy {}
+
+ /**
+ * Ranked by the document count that contains the term.
+ *
+ * <p>Suppose the following document is in the index.
+ *
+ * <pre>Doc1 contains: term1 term2 term2 term2</pre>
+ *
+ * <pre>Doc2 contains: term1</pre>
+ *
+ * <p>Then, suppose that a search suggestion for "t" is issued with the DOCUMENT_COUNT, the
+ * returned {@link SearchSuggestionResult}s will be: term1, term2. The term1 will have higher
+ * score and appear in the results first.
+ */
+ public static final int SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT = 0;
+ /**
+ * Ranked by the term appear frequency.
+ *
+ * <p>Suppose the following document is in the index.
+ *
+ * <pre>Doc1 contains: term1 term2 term2 term2</pre>
+ *
+ * <pre>Doc2 contains: term1</pre>
+ *
+ * <p>Then, suppose that a search suggestion for "t" is issued with the TERM_FREQUENCY, the
+ * returned {@link SearchSuggestionResult}s will be: term2, term1. The term2 will have higher
+ * score and appear in the results first.
+ */
+ public static final int SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY = 1;
+
+ /** No Ranking, results are returned in arbitrary order. */
+ public static final int SUGGESTION_RANKING_STRATEGY_NONE = 2;
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Returns the maximum number of wanted suggestion that will be returned in the result object.
+ */
+ public int getMaximumResultCount() {
+ return mMaximumResultCount;
+ }
+
+ /**
+ * Returns the list of namespaces to search over.
+ *
+ * <p>If empty, will search over all namespaces.
+ */
+ @NonNull
+ public List<String> getFilterNamespaces() {
+ List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
+ if (namespaces == null) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(namespaces);
+ }
+
+ /** Returns the ranking strategy. */
+ @SuggestionRankingStrategy
+ public int getRankingStrategy() {
+ return mBundle.getInt(RANKING_STRATEGY_FIELD);
+ }
+
+ /**
+ * Returns the list of schema to search the suggestion over.
+ *
+ * <p>If empty, will search over all schemas.
+ */
+ @NonNull
+ public List<String> getFilterSchemas() {
+ List<String> schemaTypes = mBundle.getStringArrayList(SCHEMA_FIELD);
+ if (schemaTypes == null) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(schemaTypes);
+ }
+
+ /**
+ * Returns the map of schema and target properties to search over.
+ *
+ * <p>The keys of the returned map are schema types, and the values are the target property path
+ * in that schema to search over.
+ *
+ * <p>If {@link Builder#addFilterPropertyPaths} was never called, returns an empty map. In this
+ * case AppSearch will search over all schemas and properties.
+ *
+ * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this
+ * function, rather than calling it multiple times.
+ *
+ * @hide
+ */
+ // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
+ @NonNull
+ public Map<String, List<String>> getFilterProperties() {
+ Bundle typePropertyPathsBundle = Objects.requireNonNull(mBundle.getBundle(PROPERTY_FIELD));
+ Set<String> schemas = typePropertyPathsBundle.keySet();
+ Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+ for (String schema : schemas) {
+ typePropertyPathsMap.put(
+ schema,
+ Objects.requireNonNull(typePropertyPathsBundle.getStringArrayList(schema)));
+ }
+ return typePropertyPathsMap;
+ }
+
+ /**
+ * Returns the map of namespace and target document ids to search over.
+ *
+ * <p>The keys of the returned map are namespaces, and the values are the target document ids in
+ * that namespace to search over.
+ *
+ * <p>If {@link Builder#addFilterDocumentIds} was never called, returns an empty map. In this
+ * case AppSearch will search over all namespace and document ids.
+ *
+ * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this
+ * function, rather than calling it multiple times.
+ */
+ @NonNull
+ public Map<String, List<String>> getFilterDocumentIds() {
+ Bundle documentIdsBundle = Objects.requireNonNull(mBundle.getBundle(DOCUMENT_IDS_FIELD));
+ Set<String> namespaces = documentIdsBundle.keySet();
+ Map<String, List<String>> documentIdsMap = new ArrayMap<>(namespaces.size());
+ for (String namespace : namespaces) {
+ documentIdsMap.put(
+ namespace,
+ Objects.requireNonNull(documentIdsBundle.getStringArrayList(namespace)));
+ }
+ return documentIdsMap;
+ }
+
+ /** Builder for {@link SearchSuggestionSpec objects}. */
+ public static final class Builder {
+ private ArrayList<String> mNamespaces = new ArrayList<>();
+ private ArrayList<String> mSchemas = new ArrayList<>();
+ private Bundle mTypePropertyFilters = new Bundle();
+ private Bundle mDocumentIds = new Bundle();
+ private final int mTotalResultCount;
+
+ @SuggestionRankingStrategy
+ private int mRankingStrategy = SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT;
+
+ private boolean mBuilt = false;
+
+ /**
+ * Creates an {@link SearchSuggestionSpec.Builder} object.
+ *
+ * @param maximumResultCount Sets the maximum number of suggestion in the returned object.
+ */
+ public Builder(@IntRange(from = 1) int maximumResultCount) {
+ Preconditions.checkArgument(
+ maximumResultCount >= 1, "maximumResultCount must be positive.");
+ mTotalResultCount = maximumResultCount;
+ }
+
+ /**
+ * Adds a namespace filter to {@link SearchSuggestionSpec} Entry. Only search for
+ * suggestions that has documents under the specified namespaces.
+ *
+ * <p>If unset, the query will search over all namespaces.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterNamespaces(@NonNull String... namespaces) {
+ Objects.requireNonNull(namespaces);
+ resetIfBuilt();
+ return addFilterNamespaces(Arrays.asList(namespaces));
+ }
+
+ /**
+ * Adds a namespace filter to {@link SearchSuggestionSpec} Entry. Only search for
+ * suggestions that has documents under the specified namespaces.
+ *
+ * <p>If unset, the query will search over all namespaces.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) {
+ Objects.requireNonNull(namespaces);
+ resetIfBuilt();
+ mNamespaces.addAll(namespaces);
+ return this;
+ }
+
+ /**
+ * Sets ranking strategy for suggestion results.
+ *
+ * <p>The default value {@link #SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT} will be used if
+ * this method is never called.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setRankingStrategy(@SuggestionRankingStrategy int rankingStrategy) {
+ Preconditions.checkArgumentInRange(
+ rankingStrategy,
+ SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT,
+ SUGGESTION_RANKING_STRATEGY_NONE,
+ "Suggestion ranking strategy");
+ resetIfBuilt();
+ mRankingStrategy = rankingStrategy;
+ return this;
+ }
+
+ /**
+ * Adds a schema filter to {@link SearchSuggestionSpec} Entry. Only search for suggestions
+ * that has documents under the specified schema.
+ *
+ * <p>If unset, the query will search over all schema.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterSchemas(@NonNull String... schemaTypes) {
+ Objects.requireNonNull(schemaTypes);
+ resetIfBuilt();
+ return addFilterSchemas(Arrays.asList(schemaTypes));
+ }
+
+ /**
+ * Adds a schema filter to {@link SearchSuggestionSpec} Entry. Only search for suggestions
+ * that has documents under the specified schema.
+ *
+ * <p>If unset, the query will search over all schema.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterSchemas(@NonNull Collection<String> schemaTypes) {
+ Objects.requireNonNull(schemaTypes);
+ resetIfBuilt();
+ mSchemas.addAll(schemaTypes);
+ return this;
+ }
+
+ /**
+ * Adds property paths for the specified type to the property filter of {@link
+ * SearchSuggestionSpec} Entry. Only search for suggestions that has content under the
+ * specified property. If property paths are added for a type, then only the properties
+ * referred to will be retrieved for results of that type.
+ *
+ * <p>If a property path that is specified isn't present in a result, it will be ignored for
+ * that result. Property paths cannot be null.
+ *
+ * <p>If no property paths are added for a particular type, then all properties of results
+ * of that type will be retrieved.
+ *
+ * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
+ *
+ * @param schema the {@link AppSearchSchema} that contains the target properties
+ * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited sequence
+ * of property names indicating which property in the document these snippets correspond
+ * to.
+ * @hide
+ */
+ // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
+ @NonNull
+ public Builder addFilterProperties(
+ @NonNull String schema, @NonNull Collection<String> propertyPaths) {
+ Objects.requireNonNull(schema);
+ Objects.requireNonNull(propertyPaths);
+ resetIfBuilt();
+ ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
+ for (String propertyPath : propertyPaths) {
+ Objects.requireNonNull(propertyPath);
+ propertyPathsArrayList.add(propertyPath);
+ }
+ mTypePropertyFilters.putStringArrayList(schema, propertyPathsArrayList);
+ return this;
+ }
+
+ /**
+ * Adds property paths for the specified type to the property filter of {@link
+ * SearchSuggestionSpec} Entry. Only search for suggestions that has content under the
+ * specified property. If property paths are added for a type, then only the properties
+ * referred to will be retrieved for results of that type.
+ *
+ * <p>If a property path that is specified isn't present in a result, it will be ignored for
+ * that result. Property paths cannot be null.
+ *
+ * <p>If no property paths are added for a particular type, then all properties of results
+ * of that type will be retrieved.
+ *
+ * @param schema the {@link AppSearchSchema} that contains the target properties
+ * @param propertyPaths The {@link PropertyPath} to search suggestion over
+ * @hide
+ */
+ // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
+ @NonNull
+ public Builder addFilterPropertyPaths(
+ @NonNull String schema, @NonNull Collection<PropertyPath> propertyPaths) {
+ Objects.requireNonNull(schema);
+ Objects.requireNonNull(propertyPaths);
+ ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
+ for (PropertyPath propertyPath : propertyPaths) {
+ propertyPathsArrayList.add(propertyPath.toString());
+ }
+ return addFilterProperties(schema, propertyPathsArrayList);
+ }
+
+ /**
+ * Adds a document ID filter to {@link SearchSuggestionSpec} Entry. Only search for
+ * suggestions in the given specified documents.
+ *
+ * <p>If unset, the query will search over all documents.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterDocumentIds(
+ @NonNull String namespace, @NonNull String... documentIds) {
+ Objects.requireNonNull(namespace);
+ Objects.requireNonNull(documentIds);
+ resetIfBuilt();
+ return addFilterDocumentIds(namespace, Arrays.asList(documentIds));
+ }
+
+ /**
+ * Adds a document ID filter to {@link SearchSuggestionSpec} Entry. Only search for
+ * suggestions in the given specified documents.
+ *
+ * <p>If unset, the query will search over all documents.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterDocumentIds(
+ @NonNull String namespace, @NonNull Collection<String> documentIds) {
+ Objects.requireNonNull(namespace);
+ Objects.requireNonNull(documentIds);
+ resetIfBuilt();
+ ArrayList<String> documentIdList = new ArrayList<>(documentIds.size());
+ for (String documentId : documentIds) {
+ documentIdList.add(Objects.requireNonNull(documentId));
+ }
+ mDocumentIds.putStringArrayList(namespace, documentIdList);
+ return this;
+ }
+
+ /** Constructs a new {@link SearchSpec} from the contents of this builder. */
+ @NonNull
+ public SearchSuggestionSpec build() {
+ Bundle bundle = new Bundle();
+ if (!mSchemas.isEmpty()) {
+ Set<String> schemaFilter = new ArraySet<>(mSchemas);
+ for (String schema : mTypePropertyFilters.keySet()) {
+ if (!schemaFilter.contains(schema)) {
+ throw new IllegalStateException(
+ "The schema: "
+ + schema
+ + " exists in the property filter but "
+ + "doesn't exist in the schema filter.");
+ }
+ }
+ }
+ if (!mNamespaces.isEmpty()) {
+ Set<String> namespaceFilter = new ArraySet<>(mNamespaces);
+ for (String namespace : mDocumentIds.keySet()) {
+ if (!namespaceFilter.contains(namespace)) {
+ throw new IllegalStateException(
+ "The namespace: "
+ + namespace
+ + " exists in the document id "
+ + "filter but doesn't exist in the namespace filter.");
+ }
+ }
+ }
+ bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
+ bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
+ bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters);
+ bundle.putBundle(DOCUMENT_IDS_FIELD, mDocumentIds);
+ bundle.putInt(MAXIMUM_RESULT_COUNT_FIELD, mTotalResultCount);
+ bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
+ mBuilt = true;
+ return new SearchSuggestionSpec(bundle);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mNamespaces = new ArrayList<>(mNamespaces);
+ mSchemas = new ArrayList<>(mSchemas);
+ mTypePropertyFilters = BundleUtil.deepCopy(mTypePropertyFilters);
+ mDocumentIds = BundleUtil.deepCopy(mDocumentIds);
+ mBuilt = false;
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/SetSchemaRequest.java b/android-34/android/app/appsearch/SetSchemaRequest.java
new file mode 100644
index 0000000..bf58d23
--- /dev/null
+++ b/android-34/android/app/appsearch/SetSchemaRequest.java
@@ -0,0 +1,641 @@
+/*
+ * Copyright 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 android.app.appsearch;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Encapsulates a request to update the schema of an {@link AppSearchSession} database.
+ *
+ * <p>The schema is composed of a collection of {@link AppSearchSchema} objects, each of which
+ * defines a unique type of data.
+ *
+ * <p>The first call to SetSchemaRequest will set the provided schema and store it within the {@link
+ * AppSearchSession} database.
+ *
+ * <p>Subsequent calls will compare the provided schema to the previously saved schema, to determine
+ * how to treat existing documents.
+ *
+ * <p>The following types of schema modifications are always safe and are made without deleting any
+ * existing documents:
+ *
+ * <ul>
+ * <li>Addition of new {@link AppSearchSchema} types
+ * <li>Addition of new properties to an existing {@link AppSearchSchema} type
+ * <li>Changing the cardinality of a property to be less restrictive
+ * </ul>
+ *
+ * <p>The following types of schema changes are not backwards compatible:
+ *
+ * <ul>
+ * <li>Removal of an existing {@link AppSearchSchema} type
+ * <li>Removal of a property from an existing {@link AppSearchSchema} type
+ * <li>Changing the data type of an existing property
+ * <li>Changing the cardinality of a property to be more restrictive
+ * </ul>
+ *
+ * <p>Providing a schema with incompatible changes, will throw an {@link
+ * android.app.appsearch.exceptions.AppSearchException}, with a message describing the
+ * incompatibility. As a result, the previously set schema will remain unchanged.
+ *
+ * <p>Backward incompatible changes can be made by :
+ *
+ * <ul>
+ * <li>setting {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}. This
+ * deletes all documents that are incompatible with the new schema. The new schema is then
+ * saved and persisted to disk.
+ * <li>Add a {@link Migrator} for each incompatible type and make no deletion. The migrator will
+ * migrate documents from its old schema version to the new version. Migrated types will be
+ * set into both {@link SetSchemaResponse#getIncompatibleTypes()} and {@link
+ * SetSchemaResponse#getMigratedTypes()}. See the migration section below.
+ * </ul>
+ *
+ * @see AppSearchSession#setSchema
+ * @see Migrator
+ */
+public final class SetSchemaRequest {
+
+ /**
+ * List of Android Permission are supported in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ *
+ * @see android.Manifest.permission
+ * @hide
+ */
+ @IntDef(
+ value = {
+ READ_SMS,
+ READ_CALENDAR,
+ READ_CONTACTS,
+ READ_EXTERNAL_STORAGE,
+ READ_HOME_APP_SEARCH_DATA,
+ READ_ASSISTANT_APP_SEARCH_DATA,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AppSearchSupportedPermission {}
+
+ /**
+ * The {@link android.Manifest.permission#READ_SMS} AppSearch supported in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ public static final int READ_SMS = 1;
+
+ /**
+ * The {@link android.Manifest.permission#READ_CALENDAR} AppSearch supported in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ public static final int READ_CALENDAR = 2;
+
+ /**
+ * The {@link android.Manifest.permission#READ_CONTACTS} AppSearch supported in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ public static final int READ_CONTACTS = 3;
+
+ /**
+ * The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} AppSearch supported in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ public static final int READ_EXTERNAL_STORAGE = 4;
+
+ /**
+ * The {@link android.Manifest.permission#READ_HOME_APP_SEARCH_DATA} AppSearch supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ public static final int READ_HOME_APP_SEARCH_DATA = 5;
+
+ /**
+ * The {@link android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA} AppSearch supported in
+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ */
+ public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6;
+
+ private final Set<AppSearchSchema> mSchemas;
+ private final Set<String> mSchemasNotDisplayedBySystem;
+ private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+ private final Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
+ private final Map<String, Migrator> mMigrators;
+ private final boolean mForceOverride;
+ private final int mVersion;
+
+ SetSchemaRequest(
+ @NonNull Set<AppSearchSchema> schemas,
+ @NonNull Set<String> schemasNotDisplayedBySystem,
+ @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
+ @NonNull Map<String, Set<Set<Integer>>> schemasVisibleToPermissions,
+ @NonNull Map<String, Migrator> migrators,
+ boolean forceOverride,
+ int version) {
+ mSchemas = Objects.requireNonNull(schemas);
+ mSchemasNotDisplayedBySystem = Objects.requireNonNull(schemasNotDisplayedBySystem);
+ mSchemasVisibleToPackages = Objects.requireNonNull(schemasVisibleToPackages);
+ mSchemasVisibleToPermissions = Objects.requireNonNull(schemasVisibleToPermissions);
+ mMigrators = Objects.requireNonNull(migrators);
+ mForceOverride = forceOverride;
+ mVersion = version;
+ }
+
+ /** Returns the {@link AppSearchSchema} types that are part of this request. */
+ @NonNull
+ public Set<AppSearchSchema> getSchemas() {
+ return Collections.unmodifiableSet(mSchemas);
+ }
+
+ /**
+ * Returns all the schema types that are opted out of being displayed and visible on any system
+ * UI surface.
+ */
+ @NonNull
+ public Set<String> getSchemasNotDisplayedBySystem() {
+ return Collections.unmodifiableSet(mSchemasNotDisplayedBySystem);
+ }
+
+ /**
+ * Returns a mapping of schema types to the set of packages that have access to that schema
+ * type.
+ *
+ * <p>It’s inefficient to call this method repeatedly.
+ */
+ @NonNull
+ public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackages() {
+ Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
+ for (Map.Entry<String, Set<PackageIdentifier>> entry :
+ mSchemasVisibleToPackages.entrySet()) {
+ copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+ }
+ return copy;
+ }
+
+ /**
+ * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
+ * combinations that querier must hold to access that schema type.
+ *
+ * <p>The querier could read the {@link GenericDocument} objects under the {@code schemaType} if
+ * they holds ALL required permissions of ANY of the individual value sets.
+ *
+ * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
+ * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+ *
+ * <ul>
+ * <li>A querier holds both PermissionA and PermissionB has access.
+ * <li>A querier holds both PermissionC and PermissionD has access.
+ * <li>A querier holds only PermissionE has access.
+ * <li>A querier holds both PermissionA and PermissionE has access.
+ * <li>A querier holds only PermissionA doesn't have access.
+ * <li>A querier holds both PermissionA and PermissionC doesn't have access.
+ * </ul>
+ *
+ * <p>It’s inefficient to call this method repeatedly.
+ *
+ * @return The map contains schema type and all combinations of required permission for querier
+ * to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS}, {@link
+ * SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS}, {@link
+ * SetSchemaRequest#READ_EXTERNAL_STORAGE}, {@link
+ * SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and {@link
+ * SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
+ */
+ @NonNull
+ public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
+ return deepCopy(mSchemasVisibleToPermissions);
+ }
+
+ /**
+ * Returns the map of {@link Migrator}, the key will be the schema type of the {@link Migrator}
+ * associated with.
+ */
+ @NonNull
+ public Map<String, Migrator> getMigrators() {
+ return Collections.unmodifiableMap(mMigrators);
+ }
+
+ /**
+ * Returns a mapping of {@link AppSearchSchema} types to the set of packages that have access to
+ * that schema type.
+ *
+ * <p>A more efficient version of {@link #getSchemasVisibleToPackages}, but it returns a
+ * modifiable map. This is not meant to be unhidden and should only be used by internal classes.
+ *
+ * @hide
+ */
+ @NonNull
+ public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackagesInternal() {
+ return mSchemasVisibleToPackages;
+ }
+
+ /** Returns whether this request will force the schema to be overridden. */
+ public boolean isForceOverride() {
+ return mForceOverride;
+ }
+
+ /** Returns the database overall schema version. */
+ @IntRange(from = 1)
+ public int getVersion() {
+ return mVersion;
+ }
+
+ /** Builder for {@link SetSchemaRequest} objects. */
+ public static final class Builder {
+ private static final int DEFAULT_VERSION = 1;
+ private ArraySet<AppSearchSchema> mSchemas = new ArraySet<>();
+ private ArraySet<String> mSchemasNotDisplayedBySystem = new ArraySet<>();
+ private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
+ new ArrayMap<>();
+ private ArrayMap<String, Set<Set<Integer>>> mSchemasVisibleToPermissions = new ArrayMap<>();
+ private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
+ private boolean mForceOverride = false;
+ private int mVersion = DEFAULT_VERSION;
+ private boolean mBuilt = false;
+
+ /**
+ * Adds one or more {@link AppSearchSchema} types to the schema.
+ *
+ * <p>An {@link AppSearchSchema} object represents one type of structured data.
+ *
+ * <p>Any documents of these types will be displayed on system UI surfaces by default.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addSchemas(@NonNull AppSearchSchema... schemas) {
+ Objects.requireNonNull(schemas);
+ resetIfBuilt();
+ return addSchemas(Arrays.asList(schemas));
+ }
+
+ /**
+ * Adds a collection of {@link AppSearchSchema} objects to the schema.
+ *
+ * <p>An {@link AppSearchSchema} object represents one type of structured data.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addSchemas(@NonNull Collection<AppSearchSchema> schemas) {
+ Objects.requireNonNull(schemas);
+ resetIfBuilt();
+ mSchemas.addAll(schemas);
+ return this;
+ }
+
+ /**
+ * Sets whether or not documents from the provided {@code schemaType} will be displayed and
+ * visible on any system UI surface.
+ *
+ * <p>This setting applies to the provided {@code schemaType} only, and does not persist
+ * across {@link AppSearchSession#setSchema} calls.
+ *
+ * <p>The default behavior, if this method is not called, is to allow types to be displayed
+ * on system UI surfaces.
+ *
+ * @param schemaType The name of an {@link AppSearchSchema} within the same {@link
+ * SetSchemaRequest}, which will be configured.
+ * @param displayed Whether documents of this type will be displayed on system UI surfaces.
+ */
+ // Merged list available from getSchemasNotDisplayedBySystem
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setSchemaTypeDisplayedBySystem(
+ @NonNull String schemaType, boolean displayed) {
+ Objects.requireNonNull(schemaType);
+ resetIfBuilt();
+ if (displayed) {
+ mSchemasNotDisplayedBySystem.remove(schemaType);
+ } else {
+ mSchemasNotDisplayedBySystem.add(schemaType);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a set of required Android {@link android.Manifest.permission} combination to the
+ * given schema type.
+ *
+ * <p>If the querier holds ALL of the required permissions in this combination, they will
+ * have access to read {@link GenericDocument} objects of the given schema type.
+ *
+ * <p>You can call this method to add multiple permission combinations, and the querier will
+ * have access if they holds ANY of the combinations.
+ *
+ * <p>The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR}, {@link
+ * #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE}, {@link #READ_HOME_APP_SEARCH_DATA} and
+ * {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
+ *
+ * @see android.Manifest.permission#READ_SMS
+ * @see android.Manifest.permission#READ_CALENDAR
+ * @see android.Manifest.permission#READ_CONTACTS
+ * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
+ * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
+ * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
+ * @param schemaType The schema type to set visibility on.
+ * @param permissions A set of required Android permissions the caller need to hold to
+ * access {@link GenericDocument} objects that under the given schema.
+ * @throws IllegalArgumentException – if input unsupported permission.
+ */
+ // Merged list available from getRequiredPermissionsForSchemaTypeVisibility
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder addRequiredPermissionsForSchemaTypeVisibility(
+ @NonNull String schemaType,
+ @AppSearchSupportedPermission @NonNull Set<Integer> permissions) {
+ Objects.requireNonNull(schemaType);
+ Objects.requireNonNull(permissions);
+ for (int permission : permissions) {
+ Preconditions.checkArgumentInRange(
+ permission, READ_SMS, READ_ASSISTANT_APP_SEARCH_DATA, "permission");
+ }
+ resetIfBuilt();
+ Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
+ if (visibleToPermissions == null) {
+ visibleToPermissions = new ArraySet<>();
+ mSchemasVisibleToPermissions.put(schemaType, visibleToPermissions);
+ }
+ visibleToPermissions.add(permissions);
+ return this;
+ }
+
+ /** Clears all required permissions combinations for the given schema type. */
+ @NonNull
+ public Builder clearRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType) {
+ Objects.requireNonNull(schemaType);
+ resetIfBuilt();
+ mSchemasVisibleToPermissions.remove(schemaType);
+ return this;
+ }
+
+ /**
+ * Sets whether or not documents from the provided {@code schemaType} can be read by the
+ * specified package.
+ *
+ * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
+ * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+ *
+ * <p>To opt into one-way data sharing with another application, the developer will need to
+ * explicitly grant the other application’s package name and certificate Read access to its
+ * data.
+ *
+ * <p>For two-way data sharing, both applications need to explicitly grant Read access to
+ * one another.
+ *
+ * <p>By default, data sharing between applications is disabled.
+ *
+ * @param schemaType The schema type to set visibility on.
+ * @param visible Whether the {@code schemaType} will be visible or not.
+ * @param packageIdentifier Represents the package that will be granted visibility.
+ */
+ // Merged list available from getSchemasVisibleToPackages
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setSchemaTypeVisibilityForPackage(
+ @NonNull String schemaType,
+ boolean visible,
+ @NonNull PackageIdentifier packageIdentifier) {
+ Objects.requireNonNull(schemaType);
+ Objects.requireNonNull(packageIdentifier);
+ resetIfBuilt();
+
+ Set<PackageIdentifier> packageIdentifiers = mSchemasVisibleToPackages.get(schemaType);
+ if (visible) {
+ if (packageIdentifiers == null) {
+ packageIdentifiers = new ArraySet<>();
+ }
+ packageIdentifiers.add(packageIdentifier);
+ mSchemasVisibleToPackages.put(schemaType, packageIdentifiers);
+ } else {
+ if (packageIdentifiers == null) {
+ // Return early since there was nothing set to begin with.
+ return this;
+ }
+ packageIdentifiers.remove(packageIdentifier);
+ if (packageIdentifiers.isEmpty()) {
+ // Remove the entire key so that we don't have empty sets as values.
+ mSchemasVisibleToPackages.remove(schemaType);
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Sets the {@link Migrator} associated with the given SchemaType.
+ *
+ * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
+ * from the current version number stored in AppSearch to the final version set via {@link
+ * #setVersion}.
+ *
+ * <p>A {@link Migrator} will be invoked if the current version number stored in AppSearch
+ * is different from the final version set via {@link #setVersion} and {@link
+ * Migrator#shouldMigrate} returns {@code true}.
+ *
+ * <p>The target schema type of the output {@link GenericDocument} of {@link
+ * Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this {@link
+ * SetSchemaRequest}.
+ *
+ * @param schemaType The schema type to set migrator on.
+ * @param migrator The migrator translates a document from its current version to the final
+ * version set via {@link #setVersion}.
+ * @see SetSchemaRequest.Builder#setVersion
+ * @see SetSchemaRequest.Builder#addSchemas
+ * @see AppSearchSession#setSchema
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder") // Getter return plural objects.
+ public Builder setMigrator(@NonNull String schemaType, @NonNull Migrator migrator) {
+ Objects.requireNonNull(schemaType);
+ Objects.requireNonNull(migrator);
+ resetIfBuilt();
+ mMigrators.put(schemaType, migrator);
+ return this;
+ }
+
+ /**
+ * Sets a Map of {@link Migrator}s.
+ *
+ * <p>The key of the map is the schema type that the {@link Migrator} value applies to.
+ *
+ * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
+ * from the current version number stored in AppSearch to the final version set via {@link
+ * #setVersion}.
+ *
+ * <p>A {@link Migrator} will be invoked if the current version number stored in AppSearch
+ * is different from the final version set via {@link #setVersion} and {@link
+ * Migrator#shouldMigrate} returns {@code true}.
+ *
+ * <p>The target schema type of the output {@link GenericDocument} of {@link
+ * Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this {@link
+ * SetSchemaRequest}.
+ *
+ * @param migrators A {@link Map} of migrators that translate a document from its current
+ * version to the final version set via {@link #setVersion}. The key of the map is the
+ * schema type that the {@link Migrator} value applies to.
+ * @see SetSchemaRequest.Builder#setVersion
+ * @see SetSchemaRequest.Builder#addSchemas
+ * @see AppSearchSession#setSchema
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
+ Objects.requireNonNull(migrators);
+ resetIfBuilt();
+ mMigrators.putAll(migrators);
+ return this;
+ }
+
+ /**
+ * Sets whether or not to override the current schema in the {@link AppSearchSession}
+ * database.
+ *
+ * <p>Call this method whenever backward incompatible changes need to be made by setting
+ * {@code forceOverride} to {@code true}. As a result, during execution of the setSchema
+ * operation, all documents that are incompatible with the new schema will be deleted and
+ * the new schema will be saved and persisted.
+ *
+ * <p>By default, this is {@code false}.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setForceOverride(boolean forceOverride) {
+ resetIfBuilt();
+ mForceOverride = forceOverride;
+ return this;
+ }
+
+ /**
+ * Sets the version number of the overall {@link AppSearchSchema} in the database.
+ *
+ * <p>The {@link AppSearchSession} database can only ever hold documents for one version at
+ * a time.
+ *
+ * <p>Setting a version number that is different from the version number currently stored in
+ * AppSearch will result in AppSearch calling the {@link Migrator}s provided to {@link
+ * AppSearchSession#setSchema} to migrate the documents already in AppSearch from the
+ * previous version to the one set in this request. The version number can be updated
+ * without any other changes to the set of schemas.
+ *
+ * <p>The version number can stay the same, increase, or decrease relative to the current
+ * version number that is already stored in the {@link AppSearchSession} database.
+ *
+ * <p>The version of an empty database will always be 0. You cannot set version to the
+ * {@link SetSchemaRequest}, if it doesn't contains any {@link AppSearchSchema}.
+ *
+ * @param version A positive integer representing the version of the entire set of schemas
+ * represents the version of the whole schema in the {@link AppSearchSession} database,
+ * default version is 1.
+ * @throws IllegalArgumentException if the version is negative.
+ * @see AppSearchSession#setSchema
+ * @see Migrator
+ * @see SetSchemaRequest.Builder#setMigrator
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setVersion(@IntRange(from = 1) int version) {
+ Preconditions.checkArgument(version >= 1, "Version must be a positive number.");
+ resetIfBuilt();
+ mVersion = version;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link SetSchemaRequest} object.
+ *
+ * @throws IllegalArgumentException if schema types were referenced, but the corresponding
+ * {@link AppSearchSchema} type was never added.
+ */
+ @NonNull
+ public SetSchemaRequest build() {
+ // Verify that any schema types with display or visibility settings refer to a real
+ // schema.
+ // Create a copy because we're going to remove from the set for verification purposes.
+ Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
+ referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
+ referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet());
+
+ for (AppSearchSchema schema : mSchemas) {
+ referencedSchemas.remove(schema.getSchemaType());
+ }
+ if (!referencedSchemas.isEmpty()) {
+ // We still have schema types that weren't seen in our mSchemas set. This means
+ // there wasn't a corresponding AppSearchSchema.
+ throw new IllegalArgumentException(
+ "Schema types " + referencedSchemas + " referenced, but were not added.");
+ }
+ if (mSchemas.isEmpty() && mVersion != DEFAULT_VERSION) {
+ throw new IllegalArgumentException(
+ "Cannot set version to the request if schema is empty.");
+ }
+ mBuilt = true;
+ return new SetSchemaRequest(
+ mSchemas,
+ mSchemasNotDisplayedBySystem,
+ mSchemasVisibleToPackages,
+ mSchemasVisibleToPermissions,
+ mMigrators,
+ mForceOverride,
+ mVersion);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ ArrayMap<String, Set<PackageIdentifier>> schemasVisibleToPackages =
+ new ArrayMap<>(mSchemasVisibleToPackages.size());
+ for (Map.Entry<String, Set<PackageIdentifier>> entry :
+ mSchemasVisibleToPackages.entrySet()) {
+ schemasVisibleToPackages.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+ }
+ mSchemasVisibleToPackages = schemasVisibleToPackages;
+
+ mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions);
+
+ mSchemas = new ArraySet<>(mSchemas);
+ mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
+ mMigrators = new ArrayMap<>(mMigrators);
+ mBuilt = false;
+ }
+ }
+ }
+
+ static ArrayMap<String, Set<Set<Integer>>> deepCopy(
+ @NonNull Map<String, Set<Set<Integer>>> original) {
+ ArrayMap<String, Set<Set<Integer>>> copy = new ArrayMap<>(original.size());
+ for (Map.Entry<String, Set<Set<Integer>>> entry : original.entrySet()) {
+ Set<Set<Integer>> valueCopy = new ArraySet<>();
+ for (Set<Integer> innerValue : entry.getValue()) {
+ valueCopy.add(new ArraySet<>(innerValue));
+ }
+ copy.put(entry.getKey(), valueCopy);
+ }
+ return copy;
+ }
+}
diff --git a/android-34/android/app/appsearch/SetSchemaResponse.java b/android-34/android/app/appsearch/SetSchemaResponse.java
new file mode 100644
index 0000000..8b64132
--- /dev/null
+++ b/android-34/android/app/appsearch/SetSchemaResponse.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.os.Bundle;
+import android.util.ArraySet;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/** The response class of {@link AppSearchSession#setSchema} */
+public class SetSchemaResponse {
+
+ private static final String DELETED_TYPES_FIELD = "deletedTypes";
+ private static final String INCOMPATIBLE_TYPES_FIELD = "incompatibleTypes";
+ private static final String MIGRATED_TYPES_FIELD = "migratedTypes";
+
+ private final Bundle mBundle;
+ /**
+ * The migrationFailures won't be saved in the bundle. Since:
+ *
+ * <ul>
+ * <li>{@link MigrationFailure} is generated in {@link AppSearchSession} which will be the SDK
+ * side in platform. We don't need to pass it from service side via binder.
+ * <li>Translate multiple {@link MigrationFailure}s to bundles in {@link Builder} and then
+ * back in constructor will be a huge waste.
+ * </ul>
+ */
+ private final List<MigrationFailure> mMigrationFailures;
+
+ /** Cache of the inflated deleted schema types. Comes from inflating mBundles at first use. */
+ @Nullable private Set<String> mDeletedTypes;
+
+ /** Cache of the inflated migrated schema types. Comes from inflating mBundles at first use. */
+ @Nullable private Set<String> mMigratedTypes;
+
+ /**
+ * Cache of the inflated incompatible schema types. Comes from inflating mBundles at first use.
+ */
+ @Nullable private Set<String> mIncompatibleTypes;
+
+ SetSchemaResponse(@NonNull Bundle bundle, @NonNull List<MigrationFailure> migrationFailures) {
+ mBundle = Objects.requireNonNull(bundle);
+ mMigrationFailures = Objects.requireNonNull(migrationFailures);
+ }
+
+ SetSchemaResponse(@NonNull Bundle bundle) {
+ this(bundle, /*migrationFailures=*/ Collections.emptyList());
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Returns a {@link List} of all failed {@link MigrationFailure}.
+ *
+ * <p>A {@link MigrationFailure} will be generated if the system trying to save a post-migrated
+ * {@link GenericDocument} but fail.
+ *
+ * <p>{@link MigrationFailure} contains the namespace, id and schemaType of the post-migrated
+ * {@link GenericDocument} and the error reason. Mostly it will be mismatch the schema it
+ * migrated to.
+ */
+ @NonNull
+ public List<MigrationFailure> getMigrationFailures() {
+ return Collections.unmodifiableList(mMigrationFailures);
+ }
+
+ /**
+ * Returns a {@link Set} of deleted schema types.
+ *
+ * <p>A "deleted" type is a schema type that was previously a part of the database schema but
+ * was not present in the {@link SetSchemaRequest} object provided in the {@link
+ * AppSearchSession#setSchema} call.
+ *
+ * <p>Documents for a deleted type are removed from the database.
+ */
+ @NonNull
+ public Set<String> getDeletedTypes() {
+ if (mDeletedTypes == null) {
+ mDeletedTypes =
+ new ArraySet<>(
+ Objects.requireNonNull(
+ mBundle.getStringArrayList(DELETED_TYPES_FIELD)));
+ }
+ return Collections.unmodifiableSet(mDeletedTypes);
+ }
+
+ /**
+ * Returns a {@link Set} of schema type that were migrated by the {@link
+ * AppSearchSession#setSchema} call.
+ *
+ * <p>A "migrated" type is a schema type that has triggered a {@link Migrator} instance to
+ * migrate documents of the schema type to another schema type, or to another version of the
+ * schema type.
+ *
+ * <p>If a document fails to be migrated, a {@link MigrationFailure} will be generated for that
+ * document.
+ *
+ * @see Migrator
+ */
+ @NonNull
+ public Set<String> getMigratedTypes() {
+ if (mMigratedTypes == null) {
+ mMigratedTypes =
+ new ArraySet<>(
+ Objects.requireNonNull(
+ mBundle.getStringArrayList(MIGRATED_TYPES_FIELD)));
+ }
+ return Collections.unmodifiableSet(mMigratedTypes);
+ }
+
+ /**
+ * Returns a {@link Set} of schema type whose new definitions set in the {@link
+ * AppSearchSession#setSchema} call were incompatible with the pre-existing schema.
+ *
+ * <p>If a {@link Migrator} is provided for this type and the migration is success triggered.
+ * The type will also appear in {@link #getMigratedTypes()}.
+ *
+ * @see SetSchemaRequest
+ * @see AppSearchSession#setSchema
+ * @see SetSchemaRequest.Builder#setForceOverride
+ */
+ @NonNull
+ public Set<String> getIncompatibleTypes() {
+ if (mIncompatibleTypes == null) {
+ mIncompatibleTypes =
+ new ArraySet<>(
+ Objects.requireNonNull(
+ mBundle.getStringArrayList(INCOMPATIBLE_TYPES_FIELD)));
+ }
+ return Collections.unmodifiableSet(mIncompatibleTypes);
+ }
+
+ /**
+ * Translates the {@link SetSchemaResponse}'s bundle to {@link Builder}.
+ *
+ * @hide
+ */
+ @NonNull
+ // TODO(b/179302942) change to Builder(mBundle) powered by mBundle.deepCopy
+ public Builder toBuilder() {
+ return new Builder()
+ .addDeletedTypes(getDeletedTypes())
+ .addIncompatibleTypes(getIncompatibleTypes())
+ .addMigratedTypes(getMigratedTypes())
+ .addMigrationFailures(mMigrationFailures);
+ }
+
+ /** Builder for {@link SetSchemaResponse} objects. */
+ public static final class Builder {
+ private List<MigrationFailure> mMigrationFailures = new ArrayList<>();
+ private ArrayList<String> mDeletedTypes = new ArrayList<>();
+ private ArrayList<String> mMigratedTypes = new ArrayList<>();
+ private ArrayList<String> mIncompatibleTypes = new ArrayList<>();
+ private boolean mBuilt = false;
+
+ /** Adds {@link MigrationFailure}s to the list of migration failures. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addMigrationFailures(
+ @NonNull Collection<MigrationFailure> migrationFailures) {
+ Objects.requireNonNull(migrationFailures);
+ resetIfBuilt();
+ mMigrationFailures.addAll(migrationFailures);
+ return this;
+ }
+
+ /** Adds a {@link MigrationFailure} to the list of migration failures. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addMigrationFailure(@NonNull MigrationFailure migrationFailure) {
+ Objects.requireNonNull(migrationFailure);
+ resetIfBuilt();
+ mMigrationFailures.add(migrationFailure);
+ return this;
+ }
+
+ /** Adds deletedTypes to the list of deleted schema types. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addDeletedTypes(@NonNull Collection<String> deletedTypes) {
+ Objects.requireNonNull(deletedTypes);
+ resetIfBuilt();
+ mDeletedTypes.addAll(deletedTypes);
+ return this;
+ }
+
+ /** Adds one deletedType to the list of deleted schema types. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addDeletedType(@NonNull String deletedType) {
+ Objects.requireNonNull(deletedType);
+ resetIfBuilt();
+ mDeletedTypes.add(deletedType);
+ return this;
+ }
+
+ /** Adds incompatibleTypes to the list of incompatible schema types. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addIncompatibleTypes(@NonNull Collection<String> incompatibleTypes) {
+ Objects.requireNonNull(incompatibleTypes);
+ resetIfBuilt();
+ mIncompatibleTypes.addAll(incompatibleTypes);
+ return this;
+ }
+
+ /** Adds one incompatibleType to the list of incompatible schema types. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addIncompatibleType(@NonNull String incompatibleType) {
+ Objects.requireNonNull(incompatibleType);
+ resetIfBuilt();
+ mIncompatibleTypes.add(incompatibleType);
+ return this;
+ }
+
+ /** Adds migratedTypes to the list of migrated schema types. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addMigratedTypes(@NonNull Collection<String> migratedTypes) {
+ Objects.requireNonNull(migratedTypes);
+ resetIfBuilt();
+ mMigratedTypes.addAll(migratedTypes);
+ return this;
+ }
+
+ /** Adds one migratedType to the list of migrated schema types. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addMigratedType(@NonNull String migratedType) {
+ Objects.requireNonNull(migratedType);
+ resetIfBuilt();
+ mMigratedTypes.add(migratedType);
+ return this;
+ }
+
+ /** Builds a {@link SetSchemaResponse} object. */
+ @NonNull
+ public SetSchemaResponse build() {
+ Bundle bundle = new Bundle();
+ bundle.putStringArrayList(INCOMPATIBLE_TYPES_FIELD, mIncompatibleTypes);
+ bundle.putStringArrayList(DELETED_TYPES_FIELD, mDeletedTypes);
+ bundle.putStringArrayList(MIGRATED_TYPES_FIELD, mMigratedTypes);
+ mBuilt = true;
+ // Avoid converting the potential thousands of MigrationFailures to Pracelable and
+ // back just for put in bundle. In platform, we should set MigrationFailures in
+ // AppSearchSession after we pass SetSchemaResponse via binder.
+ return new SetSchemaResponse(bundle, mMigrationFailures);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mMigrationFailures = new ArrayList<>(mMigrationFailures);
+ mDeletedTypes = new ArrayList<>(mDeletedTypes);
+ mMigratedTypes = new ArrayList<>(mMigratedTypes);
+ mIncompatibleTypes = new ArrayList<>(mIncompatibleTypes);
+ mBuilt = false;
+ }
+ }
+ }
+
+ /**
+ * The class represents a post-migrated {@link GenericDocument} that failed to be saved by
+ * {@link AppSearchSession#setSchema}.
+ */
+ public static class MigrationFailure {
+ private static final String SCHEMA_TYPE_FIELD = "schemaType";
+ private static final String NAMESPACE_FIELD = "namespace";
+ private static final String DOCUMENT_ID_FIELD = "id";
+ private static final String ERROR_MESSAGE_FIELD = "errorMessage";
+ private static final String RESULT_CODE_FIELD = "resultCode";
+
+ private final Bundle mBundle;
+
+ /**
+ * Constructs a new {@link MigrationFailure}.
+ *
+ * @param namespace The namespace of the document which failed to be migrated.
+ * @param documentId The id of the document which failed to be migrated.
+ * @param schemaType The type of the document which failed to be migrated.
+ * @param failedResult The reason why the document failed to be indexed.
+ * @throws IllegalArgumentException if the provided {@code failedResult} was not a failure.
+ */
+ public MigrationFailure(
+ @NonNull String namespace,
+ @NonNull String documentId,
+ @NonNull String schemaType,
+ @NonNull AppSearchResult<?> failedResult) {
+ mBundle = new Bundle();
+ mBundle.putString(NAMESPACE_FIELD, Objects.requireNonNull(namespace));
+ mBundle.putString(DOCUMENT_ID_FIELD, Objects.requireNonNull(documentId));
+ mBundle.putString(SCHEMA_TYPE_FIELD, Objects.requireNonNull(schemaType));
+
+ Objects.requireNonNull(failedResult);
+ Preconditions.checkArgument(
+ !failedResult.isSuccess(), "failedResult was actually successful");
+ mBundle.putString(ERROR_MESSAGE_FIELD, failedResult.getErrorMessage());
+ mBundle.putInt(RESULT_CODE_FIELD, failedResult.getResultCode());
+ }
+
+ MigrationFailure(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ /**
+ * Returns the Bundle of the {@link MigrationFailure}.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */
+ @NonNull
+ public String getNamespace() {
+ return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/ "");
+ }
+
+ /** Returns the id of the {@link GenericDocument} that failed to be migrated. */
+ @NonNull
+ public String getDocumentId() {
+ return mBundle.getString(DOCUMENT_ID_FIELD, /*defaultValue=*/ "");
+ }
+
+ /** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */
+ @NonNull
+ public String getSchemaType() {
+ return mBundle.getString(SCHEMA_TYPE_FIELD, /*defaultValue=*/ "");
+ }
+
+ /**
+ * Returns the {@link AppSearchResult} that indicates why the post-migration {@link
+ * GenericDocument} failed to be indexed.
+ */
+ @NonNull
+ public AppSearchResult<Void> getAppSearchResult() {
+ return AppSearchResult.newFailedResult(
+ mBundle.getInt(RESULT_CODE_FIELD),
+ mBundle.getString(ERROR_MESSAGE_FIELD, /*defaultValue=*/ ""));
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "MigrationFailure { schemaType: "
+ + getSchemaType()
+ + ", namespace: "
+ + getNamespace()
+ + ", documentId: "
+ + getDocumentId()
+ + ", appSearchResult: "
+ + getAppSearchResult().toString()
+ + "}";
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/StorageInfo.java b/android-34/android/app/appsearch/StorageInfo.java
new file mode 100644
index 0000000..1b3e7ee
--- /dev/null
+++ b/android-34/android/app/appsearch/StorageInfo.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.os.Bundle;
+
+import java.util.Objects;
+
+/** The response class of {@code AppSearchSession#getStorageInfo}. */
+public class StorageInfo {
+
+ private static final String SIZE_BYTES_FIELD = "sizeBytes";
+ private static final String ALIVE_DOCUMENTS_COUNT = "aliveDocumentsCount";
+ private static final String ALIVE_NAMESPACES_COUNT = "aliveNamespacesCount";
+
+ private final Bundle mBundle;
+
+ StorageInfo(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /** Returns the estimated size of the session's database in bytes. */
+ public long getSizeBytes() {
+ return mBundle.getLong(SIZE_BYTES_FIELD);
+ }
+
+ /**
+ * Returns the number of alive documents in the current session.
+ *
+ * <p>Alive documents are documents that haven't been deleted and haven't exceeded the ttl as
+ * set in {@link GenericDocument.Builder#setTtlMillis}.
+ */
+ public int getAliveDocumentsCount() {
+ return mBundle.getInt(ALIVE_DOCUMENTS_COUNT);
+ }
+
+ /**
+ * Returns the number of namespaces that have at least one alive document in the current
+ * session's database.
+ *
+ * <p>Alive documents are documents that haven't been deleted and haven't exceeded the ttl as
+ * set in {@link GenericDocument.Builder#setTtlMillis}.
+ */
+ public int getAliveNamespacesCount() {
+ return mBundle.getInt(ALIVE_NAMESPACES_COUNT);
+ }
+
+ /** Builder for {@link StorageInfo} objects. */
+ public static final class Builder {
+ private long mSizeBytes;
+ private int mAliveDocumentsCount;
+ private int mAliveNamespacesCount;
+
+ /** Sets the size in bytes. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public StorageInfo.Builder setSizeBytes(long sizeBytes) {
+ mSizeBytes = sizeBytes;
+ return this;
+ }
+
+ /** Sets the number of alive documents. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public StorageInfo.Builder setAliveDocumentsCount(int aliveDocumentsCount) {
+ mAliveDocumentsCount = aliveDocumentsCount;
+ return this;
+ }
+
+ /** Sets the number of alive namespaces. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public StorageInfo.Builder setAliveNamespacesCount(int aliveNamespacesCount) {
+ mAliveNamespacesCount = aliveNamespacesCount;
+ return this;
+ }
+
+ /** Builds a {@link StorageInfo} object. */
+ @NonNull
+ public StorageInfo build() {
+ Bundle bundle = new Bundle();
+ bundle.putLong(SIZE_BYTES_FIELD, mSizeBytes);
+ bundle.putInt(ALIVE_DOCUMENTS_COUNT, mAliveDocumentsCount);
+ bundle.putInt(ALIVE_NAMESPACES_COUNT, mAliveNamespacesCount);
+ return new StorageInfo(bundle);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/VisibilityDocument.java b/android-34/android/app/appsearch/VisibilityDocument.java
new file mode 100644
index 0000000..b9b7f68
--- /dev/null
+++ b/android-34/android/app/appsearch/VisibilityDocument.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.os.Bundle;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Holds the visibility settings that apply to a schema type.
+ *
+ * @hide
+ */
+public class VisibilityDocument extends GenericDocument {
+ /** The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings. */
+ public static final String SCHEMA_TYPE = "VisibilityType";
+ /** Namespace of documents that contain visibility settings */
+ public static final String NAMESPACE = "";
+
+ /**
+ * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+ */
+ private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";
+
+ /** Property that holds the package name that can access a schema. */
+ private static final String PACKAGE_NAME_PROPERTY = "packageName";
+
+ /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+ private static final String SHA_256_CERT_PROPERTY = "sha256Cert";
+
+ /** Property that holds the required permissions to access the schema. */
+ private static final String PERMISSION_PROPERTY = "permission";
+
+ // The initial schema version, one VisibilityDocument contains all visibility information for
+ // whole package.
+ public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;
+
+ // One VisibilityDocument contains visibility information for a single schema.
+ public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;
+
+ // One VisibilityDocument contains visibility information for a single schema.
+ public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;
+
+ public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;
+
+ /**
+ * Schema for the VisibilityStore's documents.
+ *
+ * <p>NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}.
+ */
+ public static final AppSearchSchema SCHEMA =
+ new AppSearchSchema.Builder(SCHEMA_TYPE)
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .addProperty(
+ new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(
+ new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .addProperty(
+ new AppSearchSchema.DocumentPropertyConfig.Builder(
+ PERMISSION_PROPERTY,
+ VisibilityPermissionDocument.SCHEMA_TYPE)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+
+ public VisibilityDocument(@NonNull GenericDocument genericDocument) {
+ super(genericDocument);
+ }
+
+ public VisibilityDocument(@NonNull Bundle bundle) {
+ super(bundle);
+ }
+
+ /** Returns whether this schema is visible to the system. */
+ public boolean isNotDisplayedBySystem() {
+ return getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
+ }
+
+ /**
+ * Returns a package name array which could access this schema. Use {@link #getSha256Certs()} to
+ * get package's sha 256 certs. The same index of package names array and sha256Certs array
+ * represents same package.
+ */
+ @NonNull
+ public String[] getPackageNames() {
+ return Objects.requireNonNull(getPropertyStringArray(PACKAGE_NAME_PROPERTY));
+ }
+
+ /**
+ * Returns a package sha256Certs array which could access this schema. Use {@link
+ * #getPackageNames()} to get package's name. The same index of package names array and
+ * sha256Certs array represents same package.
+ */
+ @NonNull
+ public byte[][] getSha256Certs() {
+ return Objects.requireNonNull(getPropertyBytesArray(SHA_256_CERT_PROPERTY));
+ }
+
+ /**
+ * Returns an array of Android Permissions that caller mush hold to access the schema this
+ * {@link VisibilityDocument} represents.
+ */
+ @Nullable
+ public Set<Set<Integer>> getVisibleToPermissions() {
+ GenericDocument[] permissionDocuments = getPropertyDocumentArray(PERMISSION_PROPERTY);
+ if (permissionDocuments == null) {
+ return Collections.emptySet();
+ }
+ Set<Set<Integer>> visibleToPermissions = new ArraySet<>(permissionDocuments.length);
+ for (GenericDocument permissionDocument : permissionDocuments) {
+ Set<Integer> requiredPermissions =
+ new VisibilityPermissionDocument(permissionDocument)
+ .getAllRequiredPermissions();
+ if (requiredPermissions != null) {
+ visibleToPermissions.add(requiredPermissions);
+ }
+ }
+ return visibleToPermissions;
+ }
+
+ /** Builder for {@link VisibilityDocument}. */
+ public static class Builder extends GenericDocument.Builder<Builder> {
+ private final Set<PackageIdentifier> mPackageIdentifiers = new ArraySet<>();
+
+ /**
+ * Creates a {@link Builder} for a {@link VisibilityDocument}.
+ *
+ * @param id The SchemaType of the {@link AppSearchSchema} that this {@link
+ * VisibilityDocument} represents. The package and database prefix will be added in
+ * server side. We are using prefixed schema type to be the final id of this {@link
+ * VisibilityDocument}.
+ */
+ public Builder(@NonNull String id) {
+ super(NAMESPACE, id, SCHEMA_TYPE);
+ }
+
+ /** Sets whether this schema has opted out of platform surfacing. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
+ return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY, notDisplayedBySystem);
+ }
+
+ /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
+ Objects.requireNonNull(packageIdentifiers);
+ mPackageIdentifiers.addAll(packageIdentifiers);
+ return this;
+ }
+
+ /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
+ Objects.requireNonNull(packageIdentifier);
+ mPackageIdentifiers.add(packageIdentifier);
+ return this;
+ }
+
+ /**
+ * Set required permission sets for a package needs to hold to the schema this {@link
+ * VisibilityDocument} represents.
+ *
+ * <p>The querier could have access if they holds ALL required permissions of ANY of the
+ * individual value sets.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setVisibleToPermissions(@NonNull Set<Set<Integer>> visibleToPermissions) {
+ Objects.requireNonNull(visibleToPermissions);
+ VisibilityPermissionDocument[] permissionDocuments =
+ new VisibilityPermissionDocument[visibleToPermissions.size()];
+ int i = 0;
+ for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+ permissionDocuments[i++] =
+ new VisibilityPermissionDocument.Builder(
+ NAMESPACE, /*id=*/ String.valueOf(i))
+ .setVisibleToAllRequiredPermissions(allRequiredPermissions)
+ .build();
+ }
+ setPropertyDocument(PERMISSION_PROPERTY, permissionDocuments);
+ return this;
+ }
+
+ /** Build a {@link VisibilityDocument} */
+ @Override
+ @NonNull
+ public VisibilityDocument build() {
+ String[] packageNames = new String[mPackageIdentifiers.size()];
+ byte[][] sha256Certs = new byte[mPackageIdentifiers.size()][32];
+ int i = 0;
+ for (PackageIdentifier packageIdentifier : mPackageIdentifiers) {
+ packageNames[i] = packageIdentifier.getPackageName();
+ sha256Certs[i] = packageIdentifier.getSha256Certificate();
+ ++i;
+ }
+ setPropertyString(PACKAGE_NAME_PROPERTY, packageNames);
+ setPropertyBytes(SHA_256_CERT_PROPERTY, sha256Certs);
+ return new VisibilityDocument(super.build());
+ }
+ }
+
+ /** Build the List of {@link VisibilityDocument} from visibility settings. */
+ @NonNull
+ public static List<VisibilityDocument> toVisibilityDocuments(
+ @NonNull SetSchemaRequest setSchemaRequest) {
+ Set<AppSearchSchema> searchSchemas = setSchemaRequest.getSchemas();
+ Set<String> schemasNotDisplayedBySystem = setSchemaRequest.getSchemasNotDisplayedBySystem();
+ Map<String, Set<PackageIdentifier>> schemasVisibleToPackages =
+ setSchemaRequest.getSchemasVisibleToPackages();
+ Map<String, Set<Set<Integer>>> schemasVisibleToPermissions =
+ setSchemaRequest.getRequiredPermissionsForSchemaTypeVisibility();
+
+ List<VisibilityDocument> visibilityDocuments = new ArrayList<>(searchSchemas.size());
+
+ for (AppSearchSchema searchSchema : searchSchemas) {
+ String schemaType = searchSchema.getSchemaType();
+ VisibilityDocument.Builder documentBuilder =
+ new VisibilityDocument.Builder(/*id=*/ searchSchema.getSchemaType());
+ documentBuilder.setNotDisplayedBySystem(
+ schemasNotDisplayedBySystem.contains(schemaType));
+
+ if (schemasVisibleToPackages.containsKey(schemaType)) {
+ documentBuilder.addVisibleToPackages(schemasVisibleToPackages.get(schemaType));
+ }
+
+ if (schemasVisibleToPermissions.containsKey(schemaType)) {
+ documentBuilder.setVisibleToPermissions(
+ schemasVisibleToPermissions.get(schemaType));
+ }
+ visibilityDocuments.add(documentBuilder.build());
+ }
+ return visibilityDocuments;
+ }
+}
diff --git a/android-34/android/app/appsearch/VisibilityPermissionDocument.java b/android-34/android/app/appsearch/VisibilityPermissionDocument.java
new file mode 100644
index 0000000..ec00e7c
--- /dev/null
+++ b/android-34/android/app/appsearch/VisibilityPermissionDocument.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.util.ArraySet;
+
+import java.util.Set;
+
+/**
+ * The nested document that holds all required permissions for a caller need to hold to access the
+ * schema which the outer {@link VisibilityDocument} represents.
+ *
+ * @hide
+ */
+public class VisibilityPermissionDocument extends GenericDocument {
+
+ /** The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings. */
+ public static final String SCHEMA_TYPE = "VisibilityPermissionType";
+
+ /** Property that holds the required permissions to access the schema. */
+ private static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";
+
+ /**
+ * Schema for the VisibilityStore's documents.
+ *
+ * <p>NOTE: If you update this, also update {@link VisibilityDocument#SCHEMA_VERSION_LATEST}.
+ */
+ public static final AppSearchSchema SCHEMA =
+ new AppSearchSchema.Builder(SCHEMA_TYPE)
+ .addProperty(
+ new AppSearchSchema.LongPropertyConfig.Builder(
+ ALL_REQUIRED_PERMISSIONS_PROPERTY)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+ .build())
+ .build();
+
+ VisibilityPermissionDocument(@NonNull GenericDocument genericDocument) {
+ super(genericDocument);
+ }
+
+ /**
+ * Returns an array of Android Permissions that caller mush hold to access the schema that the
+ * outer {@link VisibilityDocument} represents.
+ */
+ @Nullable
+ public Set<Integer> getAllRequiredPermissions() {
+ return toInts(getPropertyLongArray(ALL_REQUIRED_PERMISSIONS_PROPERTY));
+ }
+
+ /** Builder for {@link VisibilityPermissionDocument}. */
+ public static class Builder extends GenericDocument.Builder<Builder> {
+
+ /** Creates a {@link VisibilityDocument.Builder} for a {@link VisibilityDocument}. */
+ public Builder(@NonNull String namespace, @NonNull String id) {
+ super(namespace, id, SCHEMA_TYPE);
+ }
+
+ /** Sets whether this schema has opted out of platform surfacing. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setVisibleToAllRequiredPermissions(
+ @NonNull Set<Integer> allRequiredPermissions) {
+ setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, toLongs(allRequiredPermissions));
+ return this;
+ }
+
+ /** Build a {@link VisibilityPermissionDocument} */
+ @Override
+ @NonNull
+ public VisibilityPermissionDocument build() {
+ return new VisibilityPermissionDocument(super.build());
+ }
+ }
+
+ @NonNull
+ static long[] toLongs(@NonNull Set<Integer> properties) {
+ long[] outputs = new long[properties.size()];
+ int i = 0;
+ for (int property : properties) {
+ outputs[i++] = property;
+ }
+ return outputs;
+ }
+
+ @Nullable
+ private static Set<Integer> toInts(@Nullable long[] properties) {
+ if (properties == null) {
+ return null;
+ }
+ Set<Integer> outputs = new ArraySet<>(properties.length);
+ for (long property : properties) {
+ outputs.add((int) property);
+ }
+ return outputs;
+ }
+}
diff --git a/android-34/android/app/appsearch/aidl/AppSearchBatchResultParcel.java b/android-34/android/app/appsearch/aidl/AppSearchBatchResultParcel.java
new file mode 100644
index 0000000..155968d
--- /dev/null
+++ b/android-34/android/app/appsearch/aidl/AppSearchBatchResultParcel.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.aidl;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.ParcelableUtil;
+import android.app.appsearch.AppSearchResult;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Parcelable wrapper around {@link AppSearchBatchResult}.
+ *
+ * <p>{@link AppSearchBatchResult} can contain any type of key and value, including non-parcelable
+ * values. For the specific case of sending {@link AppSearchBatchResult} across Binder, this class
+ * wraps an {@link AppSearchBatchResult} that has String keys and Parcelable values. It provides
+ * parcelability of the whole structure.
+ *
+ * @param <ValueType> The type of result object for successful calls. Must be a parcelable type.
+ * @hide
+ */
+public final class AppSearchBatchResultParcel<ValueType> implements Parcelable {
+ private final AppSearchBatchResult<String, ValueType> mResult;
+
+ /** Creates a new {@link AppSearchBatchResultParcel} from the given result. */
+ public AppSearchBatchResultParcel(@NonNull AppSearchBatchResult<String, ValueType> result) {
+ mResult = Objects.requireNonNull(result);
+ }
+
+ private AppSearchBatchResultParcel(@NonNull Parcel in) {
+ Parcel unmarshallParcel = Parcel.obtain();
+ try {
+ byte[] dataBlob = Objects.requireNonNull(ParcelableUtil.readBlob(in));
+ unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
+ unmarshallParcel.setDataPosition(0);
+ AppSearchBatchResult.Builder<String, ValueType> builder =
+ new AppSearchBatchResult.Builder<>();
+ int size = unmarshallParcel.dataSize();
+ while (unmarshallParcel.dataPosition() < size) {
+ String key = Objects.requireNonNull(unmarshallParcel.readString());
+ builder.setResult(key, (AppSearchResult<ValueType>) AppSearchResultParcel
+ .directlyReadFromParcel(unmarshallParcel));
+ }
+ mResult = builder.build();
+ } finally {
+ unmarshallParcel.recycle();
+ }
+ }
+
+ @NonNull
+ public AppSearchBatchResult<String, ValueType> getResult() {
+ return mResult;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ byte[] bytes;
+ // Create a parcel object to serialize results. So that we can use Parcel.writeBlob() to
+ // send data. WriteBlob() could take care of whether to pass data via binder directly or
+ // Android shared memory if the data is large.
+ Parcel data = Parcel.obtain();
+ try {
+ for (Map.Entry<String, AppSearchResult<ValueType>> entry
+ : mResult.getAll().entrySet()) {
+ data.writeString(entry.getKey());
+ AppSearchResultParcel.directlyWriteToParcel(data, entry.getValue());
+ }
+ bytes = data.marshall();
+ } finally {
+ data.recycle();
+ }
+ ParcelableUtil.writeBlob(dest, bytes);
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @NonNull
+ public static final Creator<AppSearchBatchResultParcel<?>> CREATOR =
+ new Creator<AppSearchBatchResultParcel<?>>() {
+ @NonNull
+ @Override
+ public AppSearchBatchResultParcel<?> createFromParcel(@NonNull Parcel in) {
+ return new AppSearchBatchResultParcel<>(in);
+ }
+
+ @NonNull
+ @Override
+ public AppSearchBatchResultParcel<?>[] newArray(int size) {
+ return new AppSearchBatchResultParcel<?>[size];
+ }
+ };
+}
diff --git a/android-34/android/app/appsearch/aidl/AppSearchResultParcel.java b/android-34/android/app/appsearch/aidl/AppSearchResultParcel.java
new file mode 100644
index 0000000..f4a41f1
--- /dev/null
+++ b/android-34/android/app/appsearch/aidl/AppSearchResultParcel.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.aidl;
+
+import android.annotation.NonNull;
+import android.app.appsearch.ParcelableUtil;
+import android.app.appsearch.AppSearchResult;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Parcelable wrapper around {@link AppSearchResult}.
+ *
+ * <p>{@link AppSearchResult} can contain any value, including non-parcelable values. For the
+ * specific case of sending {@link AppSearchResult} across Binder, this class wraps an
+ * {@link AppSearchResult} that contains a parcelable type and provides parcelability of the whole
+ * structure.
+ *
+ * @param <ValueType> The type of result object for successful calls. Must be a parcelable type.
+ * @hide
+ */
+public final class AppSearchResultParcel<ValueType> implements Parcelable {
+ private final AppSearchResult<ValueType> mResult;
+
+ /** Creates a new {@link AppSearchResultParcel} from the given result. */
+ public AppSearchResultParcel(@NonNull AppSearchResult<ValueType> result) {
+ mResult = Objects.requireNonNull(result);
+ }
+
+ private AppSearchResultParcel(@NonNull Parcel in) {
+ byte[] dataBlob = Objects.requireNonNull(ParcelableUtil.readBlob(in));
+ Parcel data = Parcel.obtain();
+ try {
+ data.unmarshall(dataBlob, 0, dataBlob.length);
+ data.setDataPosition(0);
+ mResult = (AppSearchResult<ValueType>) directlyReadFromParcel(data);
+ } finally {
+ data.recycle();
+ }
+ }
+
+ static AppSearchResult directlyReadFromParcel(@NonNull Parcel data) {
+ int resultCode = data.readInt();
+ Object resultValue = data.readValue(/*loader=*/ null);
+ String errorMessage = data.readString();
+ if (resultCode == AppSearchResult.RESULT_OK) {
+ return AppSearchResult.newSuccessfulResult(resultValue);
+ } else {
+ return AppSearchResult.newFailedResult(resultCode, errorMessage);
+ }
+ }
+
+ @NonNull
+ public AppSearchResult<ValueType> getResult() {
+ return mResult;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // Serializes the whole object, So that we can use Parcel.writeBlob() to send data.
+ // WriteBlob() could take care of whether to pass data via binder directly or Android shared
+ // memory if the data is large.
+ byte[] bytes;
+ Parcel data = Parcel.obtain();
+ try {
+ directlyWriteToParcel(data, mResult);
+ bytes = data.marshall();
+ } finally {
+ data.recycle();
+ }
+ ParcelableUtil.writeBlob(dest, bytes);
+ }
+
+ static void directlyWriteToParcel(@NonNull Parcel data, @NonNull AppSearchResult result) {
+ data.writeInt(result.getResultCode());
+ if (result.isSuccess()) {
+ data.writeValue(result.getResultValue());
+ } else {
+ data.writeValue(null);
+ }
+ data.writeString(result.getErrorMessage());
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @NonNull
+ public static final Creator<AppSearchResultParcel<?>> CREATOR =
+ new Creator<AppSearchResultParcel<?>>() {
+ @NonNull
+ @Override
+ public AppSearchResultParcel<?> createFromParcel(@NonNull Parcel in) {
+ return new AppSearchResultParcel<>(in);
+ }
+
+ @NonNull
+ @Override
+ public AppSearchResultParcel<?>[] newArray(int size) {
+ return new AppSearchResultParcel<?>[size];
+ }
+ };
+}
diff --git a/android-34/android/app/appsearch/aidl/DocumentsParcel.java b/android-34/android/app/appsearch/aidl/DocumentsParcel.java
new file mode 100644
index 0000000..f183be9
--- /dev/null
+++ b/android-34/android/app/appsearch/aidl/DocumentsParcel.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.aidl;
+
+import android.annotation.NonNull;
+import android.app.appsearch.ParcelableUtil;
+import android.app.appsearch.GenericDocument;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The Parcelable object contains a List of {@link GenericDocument}.
+ *
+ * <P>This class will batch a list of {@link GenericDocument}. If the number of documents is too
+ * large for a transact, they will be put to Android Shared Memory.
+ *
+ * @see Parcel#writeBlob(byte[])
+ * @hide
+ */
+public final class DocumentsParcel implements Parcelable {
+ private final List<GenericDocument> mDocuments;
+
+ public DocumentsParcel(@NonNull List<GenericDocument> documents) {
+ mDocuments = Objects.requireNonNull(documents);
+ }
+
+ private DocumentsParcel(@NonNull Parcel in) {
+ mDocuments = readFromParcel(in);
+ }
+
+ private List<GenericDocument> readFromParcel(Parcel source) {
+ byte[] dataBlob = ParcelableUtil.readBlob(source);
+ // Create a parcel object to un-serialize the byte array we are reading from
+ // Parcel.readBlob(). Parcel.WriteBlob() could take care of whether to pass data via
+ // binder directly or Android shared memory if the data is large.
+ Parcel unmarshallParcel = Parcel.obtain();
+ try {
+ unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
+ unmarshallParcel.setDataPosition(0);
+ // read the number of document that stored in here.
+ int size = unmarshallParcel.readInt();
+ List<GenericDocument> documentList = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ // Read document's bundle and convert them.
+ documentList.add(new GenericDocument(
+ unmarshallParcel.readBundle(getClass().getClassLoader())));
+ }
+ return documentList;
+ } finally {
+ unmarshallParcel.recycle();
+ }
+ }
+
+ public static final Creator<DocumentsParcel> CREATOR = new Creator<DocumentsParcel>() {
+ @Override
+ public DocumentsParcel createFromParcel(Parcel in) {
+ return new DocumentsParcel(in);
+ }
+
+ @Override
+ public DocumentsParcel[] newArray(int size) {
+ return new DocumentsParcel[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ byte[] documentsByteArray = serializeToByteArray();
+ ParcelableUtil.writeBlob(dest, documentsByteArray);
+ }
+
+ /**
+ * Serializes the whole object, So that we can use Parcel.writeBlob() to send data. WriteBlob()
+ * could take care of whether to pass data via binder directly or Android shared memory if the
+ * data is large.
+ */
+ @NonNull
+ private byte[] serializeToByteArray() {
+ byte[] bytes;
+ Parcel data = Parcel.obtain();
+ try {
+ // Save the number documents to the temporary Parcel object.
+ data.writeInt(mDocuments.size());
+ // Save all document's bundle to the temporary Parcel object.
+ for (int i = 0; i < mDocuments.size(); i++) {
+ data.writeBundle(mDocuments.get(i).getBundle());
+ }
+ bytes = data.marshall();
+ } finally {
+ data.recycle();
+ }
+ return bytes;
+ }
+
+ /** Returns the List of {@link GenericDocument} of this object. */
+ @NonNull
+ public List<GenericDocument> getDocuments() {
+ return mDocuments;
+ }
+}
diff --git a/android-34/android/app/appsearch/annotation/CanIgnoreReturnValue.java b/android-34/android/app/appsearch/annotation/CanIgnoreReturnValue.java
new file mode 100644
index 0000000..e9b74b2
--- /dev/null
+++ b/android-34/android/app/appsearch/annotation/CanIgnoreReturnValue.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.annotation;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the return value of the annotated API is ignorable.
+ *
+ * @hide
+ */
+@Documented
+@Target({METHOD, CONSTRUCTOR, TYPE})
+@Retention(CLASS)
+public @interface CanIgnoreReturnValue {}
diff --git a/android-34/android/app/appsearch/exceptions/AppSearchException.java b/android-34/android/app/appsearch/exceptions/AppSearchException.java
new file mode 100644
index 0000000..dad59a9
--- /dev/null
+++ b/android-34/android/app/appsearch/exceptions/AppSearchException.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 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 android.app.appsearch.exceptions;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchResult;
+
+/**
+ * An exception thrown by {@link android.app.appsearch.AppSearchSession} or a subcomponent.
+ *
+ * <p>These exceptions can be converted into a failed {@link AppSearchResult} for propagating to the
+ * client.
+ */
+public class AppSearchException extends Exception {
+ private final @AppSearchResult.ResultCode int mResultCode;
+
+ /**
+ * Initializes an {@link AppSearchException} with no message.
+ *
+ * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+ */
+ public AppSearchException(@AppSearchResult.ResultCode int resultCode) {
+ this(resultCode, /*message=*/ null);
+ }
+
+ /**
+ * Initializes an {@link AppSearchException} with a result code and message.
+ *
+ * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+ * @param message The detail message (which is saved for later retrieval by the {@link
+ * #getMessage()} method).
+ */
+ public AppSearchException(
+ @AppSearchResult.ResultCode int resultCode, @Nullable String message) {
+ this(resultCode, message, /*cause=*/ null);
+ }
+
+ /**
+ * Initializes an {@link AppSearchException} with a result code, message and cause.
+ *
+ * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+ * @param message The detail message (which is saved for later retrieval by the {@link
+ * #getMessage()} method).
+ * @param cause The cause (which is saved for later retrieval by the {@link #getCause()}
+ * method). (A null value is permitted, and indicates that the cause is nonexistent or
+ * unknown.)
+ */
+ public AppSearchException(
+ @AppSearchResult.ResultCode int resultCode,
+ @Nullable String message,
+ @Nullable Throwable cause) {
+ super(message, cause);
+ mResultCode = resultCode;
+ }
+
+ /**
+ * Returns the result code this exception was constructed with.
+ *
+ * @return One of the constants documented in {@link AppSearchResult#getResultCode}.
+ */
+ @AppSearchResult.ResultCode
+ public int getResultCode() {
+ return mResultCode;
+ }
+
+ /** Converts this {@link java.lang.Exception} into a failed {@link AppSearchResult}. */
+ @NonNull
+ public <T> AppSearchResult<T> toAppSearchResult() {
+ return AppSearchResult.newFailedResult(mResultCode, getMessage());
+ }
+}
diff --git a/android-34/android/app/appsearch/exceptions/IllegalSchemaException.java b/android-34/android/app/appsearch/exceptions/IllegalSchemaException.java
new file mode 100644
index 0000000..5f8da7f
--- /dev/null
+++ b/android-34/android/app/appsearch/exceptions/IllegalSchemaException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 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 android.app.appsearch.exceptions;
+
+import android.annotation.NonNull;
+
+/**
+ * Indicates that a {@link android.app.appsearch.AppSearchSchema} has logical inconsistencies such
+ * as unpopulated mandatory fields or illegal combinations of parameters.
+ *
+ * @hide
+ */
+public class IllegalSchemaException extends IllegalArgumentException {
+ /**
+ * Constructs a new {@link IllegalSchemaException}.
+ *
+ * @param message A developer-readable description of the issue with the bundle.
+ */
+ public IllegalSchemaException(@NonNull String message) {
+ super(message);
+ }
+}
diff --git a/android-34/android/app/appsearch/observer/DocumentChangeInfo.java b/android-34/android/app/appsearch/observer/DocumentChangeInfo.java
new file mode 100644
index 0000000..9959b90
--- /dev/null
+++ b/android-34/android/app/appsearch/observer/DocumentChangeInfo.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.observer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Contains information about an individual change detected by an {@link ObserverCallback}.
+ *
+ * <p>This class reports information about document changes, i.e. when documents were added, updated
+ * or removed.
+ *
+ * <p>Changes are grouped by package, database, schema type and namespace. Each unique combination
+ * of these items will generate a unique {@link DocumentChangeInfo}.
+ *
+ * <p>Notifications are only sent for documents whose schema type matches an observer's schema
+ * filters (as determined by {@link ObserverSpec#getFilterSchemas}).
+ *
+ * <p>Note that document changes that happen during schema migration from calling {@link
+ * android.app.appsearch.AppSearchSession#setSchema} are not reported via this class. Such changes
+ * are reported through {@link SchemaChangeInfo}.
+ */
+public final class DocumentChangeInfo {
+ private final String mPackageName;
+ private final String mDatabase;
+ private final String mNamespace;
+ private final String mSchemaName;
+ private final Set<String> mChangedDocumentIds;
+
+ /**
+ * Constructs a new {@link DocumentChangeInfo}.
+ *
+ * @param packageName The package name of the app which owns the documents that changed.
+ * @param database The database in which the documents that changed reside.
+ * @param namespace The namespace in which the documents that changed reside.
+ * @param schemaName The name of the schema type that contains the changed documents.
+ * @param changedDocumentIds The set of document IDs that have been changed as part of this
+ * notification.
+ */
+ public DocumentChangeInfo(
+ @NonNull String packageName,
+ @NonNull String database,
+ @NonNull String namespace,
+ @NonNull String schemaName,
+ @NonNull Set<String> changedDocumentIds) {
+ mPackageName = Objects.requireNonNull(packageName);
+ mDatabase = Objects.requireNonNull(database);
+ mNamespace = Objects.requireNonNull(namespace);
+ mSchemaName = Objects.requireNonNull(schemaName);
+ mChangedDocumentIds =
+ Collections.unmodifiableSet(Objects.requireNonNull(changedDocumentIds));
+ }
+
+ /** Returns the package name of the app which owns the documents that changed. */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /** Returns the database in which the documents that was changed reside. */
+ @NonNull
+ public String getDatabaseName() {
+ return mDatabase;
+ }
+
+ /** Returns the namespace of the documents that changed. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the name of the schema type that contains the changed documents. */
+ @NonNull
+ public String getSchemaName() {
+ return mSchemaName;
+ }
+
+ /**
+ * Returns the set of document IDs that have been changed as part of this notification.
+ *
+ * <p>This will never be empty.
+ */
+ @NonNull
+ public Set<String> getChangedDocumentIds() {
+ return mChangedDocumentIds;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof DocumentChangeInfo)) {
+ return false;
+ }
+
+ DocumentChangeInfo that = (DocumentChangeInfo) o;
+ return mPackageName.equals(that.mPackageName)
+ && mDatabase.equals(that.mDatabase)
+ && mNamespace.equals(that.mNamespace)
+ && mSchemaName.equals(that.mSchemaName)
+ && mChangedDocumentIds.equals(that.mChangedDocumentIds);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageName, mDatabase, mNamespace, mSchemaName, mChangedDocumentIds);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "DocumentChangeInfo{"
+ + "packageName='"
+ + mPackageName
+ + '\''
+ + ", database='"
+ + mDatabase
+ + '\''
+ + ", namespace='"
+ + mNamespace
+ + '\''
+ + ", schemaName='"
+ + mSchemaName
+ + '\''
+ + ", changedDocumentIds='"
+ + mChangedDocumentIds
+ + '\''
+ + '}';
+ }
+}
diff --git a/android-34/android/app/appsearch/observer/ObserverCallback.java b/android-34/android/app/appsearch/observer/ObserverCallback.java
new file mode 100644
index 0000000..224e36b
--- /dev/null
+++ b/android-34/android/app/appsearch/observer/ObserverCallback.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.observer;
+
+import android.annotation.NonNull;
+
+/**
+ * An interface which apps can implement to subscribe to notifications of changes to AppSearch data.
+ */
+public interface ObserverCallback {
+ /**
+ * Callback to trigger after schema changes (schema type added, updated or removed).
+ *
+ * @param changeInfo Information about the nature of the change.
+ */
+ void onSchemaChanged(@NonNull SchemaChangeInfo changeInfo);
+
+ /**
+ * Callback to trigger after document changes (documents added, updated or removed).
+ *
+ * @param changeInfo Information about the nature of the change.
+ */
+ void onDocumentChanged(@NonNull DocumentChangeInfo changeInfo);
+}
diff --git a/android-34/android/app/appsearch/observer/ObserverSpec.java b/android-34/android/app/appsearch/observer/ObserverSpec.java
new file mode 100644
index 0000000..883ff38
--- /dev/null
+++ b/android-34/android/app/appsearch/observer/ObserverSpec.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.observer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.os.Bundle;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Configures the types, namespaces and other properties that {@link ObserverCallback} instances
+ * match against.
+ */
+public final class ObserverSpec {
+ private static final String FILTER_SCHEMA_FIELD = "filterSchema";
+
+ private final Bundle mBundle;
+
+ /** Populated on first use */
+ @Nullable private volatile Set<String> mFilterSchemas;
+
+ /** @hide */
+ public ObserverSpec(@NonNull Bundle bundle) {
+ Objects.requireNonNull(bundle);
+ mBundle = bundle;
+ }
+
+ /**
+ * Returns the {@link Bundle} backing this spec.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Returns the list of schema types which observers using this spec will trigger on.
+ *
+ * <p>If empty, the observers will trigger on all schema types.
+ */
+ @NonNull
+ public Set<String> getFilterSchemas() {
+ if (mFilterSchemas == null) {
+ List<String> schemas = mBundle.getStringArrayList(FILTER_SCHEMA_FIELD);
+ if (schemas == null) {
+ mFilterSchemas = Collections.emptySet();
+ } else {
+ mFilterSchemas = Collections.unmodifiableSet(new ArraySet<>(schemas));
+ }
+ }
+ return mFilterSchemas;
+ }
+
+ /** Builder for ObserverSpec instances. */
+ public static final class Builder {
+ private ArrayList<String> mFilterSchemas = new ArrayList<>();
+ private boolean mBuilt = false;
+
+ /**
+ * Restricts an observer using this spec to triggering only for documents of one of the
+ * provided schema types.
+ *
+ * <p>If unset, the observer will match documents of all types.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterSchemas(@NonNull String... schemas) {
+ Objects.requireNonNull(schemas);
+ resetIfBuilt();
+ return addFilterSchemas(Arrays.asList(schemas));
+ }
+
+ /**
+ * Restricts an observer using this spec to triggering only for documents of one of the
+ * provided schema types.
+ *
+ * <p>If unset, the observer will match documents of all types.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder addFilterSchemas(@NonNull Collection<String> schemas) {
+ Objects.requireNonNull(schemas);
+ resetIfBuilt();
+ mFilterSchemas.addAll(schemas);
+ return this;
+ }
+
+ /** Constructs a new {@link ObserverSpec} from the contents of this builder. */
+ @NonNull
+ public ObserverSpec build() {
+ Bundle bundle = new Bundle();
+ bundle.putStringArrayList(FILTER_SCHEMA_FIELD, mFilterSchemas);
+ mBuilt = true;
+ return new ObserverSpec(bundle);
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mFilterSchemas = new ArrayList<>(mFilterSchemas);
+ mBuilt = false;
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/observer/SchemaChangeInfo.java b/android-34/android/app/appsearch/observer/SchemaChangeInfo.java
new file mode 100644
index 0000000..40f82e0
--- /dev/null
+++ b/android-34/android/app/appsearch/observer/SchemaChangeInfo.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.observer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Contains information about a schema change detected by an {@link ObserverCallback}.
+ *
+ * <p>This object will be sent when a schema type having a name matching an observer's schema
+ * filters (as determined by {@link ObserverSpec#getFilterSchemas}) has been added, updated, or
+ * removed.
+ *
+ * <p>Note that schema changes may cause documents to be migrated or removed. When this happens,
+ * individual document updates will NOT be dispatched via {@link DocumentChangeInfo}. The only
+ * notification will be of the schema type change via {@link SchemaChangeInfo}. Depending on your
+ * use case, you may need to re-query the whole schema type when this happens.
+ */
+public final class SchemaChangeInfo {
+ private final String mPackageName;
+ private final String mDatabaseName;
+ private final Set<String> mChangedSchemaNames;
+
+ /**
+ * Constructs a new {@link SchemaChangeInfo}.
+ *
+ * @param packageName The package name of the app which owns the schema that changed.
+ * @param databaseName The database in which the schema that changed resides.
+ * @param changedSchemaNames Names of schemas that have changed as part of this notification.
+ */
+ public SchemaChangeInfo(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull Set<String> changedSchemaNames) {
+ mPackageName = Objects.requireNonNull(packageName);
+ mDatabaseName = Objects.requireNonNull(databaseName);
+ mChangedSchemaNames =
+ Collections.unmodifiableSet(Objects.requireNonNull(changedSchemaNames));
+ }
+
+ /** Returns the package name of the app which owns the schema that changed. */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /** Returns the database in which the schema that was changed resides. */
+ @NonNull
+ public String getDatabaseName() {
+ return mDatabaseName;
+ }
+
+ /**
+ * Returns the names of schema types affected by this change notification.
+ *
+ * <p>This will never be empty.
+ */
+ @NonNull
+ public Set<String> getChangedSchemaNames() {
+ return mChangedSchemaNames;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof SchemaChangeInfo)) {
+ return false;
+ }
+
+ SchemaChangeInfo that = (SchemaChangeInfo) o;
+ return mPackageName.equals(that.mPackageName)
+ && mDatabaseName.equals(that.mDatabaseName)
+ && mChangedSchemaNames.equals(that.mChangedSchemaNames);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageName, mDatabaseName, mChangedSchemaNames);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "SchemaChangeInfo{"
+ + "packageName='"
+ + mPackageName
+ + '\''
+ + ", databaseName='"
+ + mDatabaseName
+ + '\''
+ + ", changedSchemaNames='"
+ + mChangedSchemaNames
+ + '\''
+ + '}';
+ }
+}
diff --git a/android-34/android/app/appsearch/stats/SchemaMigrationStats.java b/android-34/android/app/appsearch/stats/SchemaMigrationStats.java
new file mode 100644
index 0000000..555bd67
--- /dev/null
+++ b/android-34/android/app/appsearch/stats/SchemaMigrationStats.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.stats;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.util.BundleUtil;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Class holds detailed stats for Schema migration.
+ *
+ * @hide
+ */
+public final class SchemaMigrationStats {
+
+ // Indicate the how a SetSchema call relative to SchemaMigration case.
+ @IntDef(
+ value = {
+ NO_MIGRATION,
+ FIRST_CALL_GET_INCOMPATIBLE,
+ SECOND_CALL_APPLY_NEW_SCHEMA,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SchemaMigrationCallType {}
+
+ /** This SetSchema call is not relative to a SchemaMigration case. */
+ public static final int NO_MIGRATION = 0;
+ /** This is the first SetSchema call in Migration cases to get all incompatible changes. */
+ public static final int FIRST_CALL_GET_INCOMPATIBLE = 1;
+ /** This is the second SetSchema call in Migration cases to apply new schema changes */
+ public static final int SECOND_CALL_APPLY_NEW_SCHEMA = 2;
+
+ private static final String PACKAGE_NAME_FIELD = "packageName";
+ private static final String DATABASE_FIELD = "database";
+ private static final String STATUS_CODE_FIELD = "StatusCode";
+ private static final String EXECUTOR_ACQUISITION_MILLIS_FIELD =
+ "ExecutorAcquisitionLatencyMillis";
+ private static final String TOTAL_LATENCY_MILLIS_FIELD = "totalLatencyMillis";
+ private static final String GET_SCHEMA_LATENCY_MILLIS_FIELD = "getSchemaLatencyMillis";
+ private static final String QUERY_AND_TRANSFORM_LATENCY_MILLIS_FIELD =
+ "queryAndTransformLatencyMillis";
+ private static final String FIRST_SET_SCHEMA_LATENCY_MILLIS_FIELD =
+ "firstSetSchemaLatencyMillis";
+ private static final String IS_FIRST_SET_SCHEMA_SUCCESS_FIELD = "isFirstSetSchemaSuccess";
+ private static final String SECOND_SET_SCHEMA_LATENCY_MILLIS_FIELD =
+ "secondSetSchemaLatencyMillis";
+ private static final String SAVE_DOCUMENT_LATENCY_MILLIS_FIELD = "saveDocumentLatencyMillis";
+ private static final String TOTAL_NEED_MIGRATED_DOCUMENT_COUNT_FIELD =
+ "totalNeedMigratedDocumentCount";
+ private static final String MIGRATION_FAILURE_COUNT_FIELD = "migrationFailureCount";
+ private static final String TOTAL_SUCCESS_MIGRATED_DOCUMENT_COUNT_FIELD =
+ "totalSuccessMigratedDocumentCount";
+
+ /**
+ * Contains all {@link SchemaMigrationStats} information in a packaged format.
+ *
+ * <p>Keys are the {@code *_FIELD} constants in this class.
+ */
+ @NonNull final Bundle mBundle;
+
+ /** Build a {@link SchemaMigrationStats} from the given bundle. */
+ public SchemaMigrationStats(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ /**
+ * Returns the {@link Bundle} populated by this builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /** Returns calling package name. */
+ @NonNull
+ public String getPackageName() {
+ return mBundle.getString(PACKAGE_NAME_FIELD);
+ }
+
+ /** Returns calling database name. */
+ @NonNull
+ public String getDatabase() {
+ return mBundle.getString(DATABASE_FIELD);
+ }
+
+ /** Returns status of the schema migration action. */
+ @AppSearchResult.ResultCode
+ public int getStatusCode() {
+ return mBundle.getInt(STATUS_CODE_FIELD);
+ }
+
+ /** Gets the latency for waiting the executor. */
+ public int getExecutorAcquisitionLatencyMillis() {
+ return mBundle.getInt(EXECUTOR_ACQUISITION_MILLIS_FIELD);
+ }
+
+ /** Gets total latency for the schema migration action in milliseconds. */
+ public int getTotalLatencyMillis() {
+ return mBundle.getInt(TOTAL_LATENCY_MILLIS_FIELD);
+ }
+
+ /** Returns GetSchema latency in milliseconds. */
+ public int getGetSchemaLatencyMillis() {
+ return mBundle.getInt(GET_SCHEMA_LATENCY_MILLIS_FIELD);
+ }
+
+ /**
+ * Returns latency of querying all documents that need to be migrated to new version and
+ * transforming documents to new version in milliseconds.
+ */
+ public int getQueryAndTransformLatencyMillis() {
+ return mBundle.getInt(QUERY_AND_TRANSFORM_LATENCY_MILLIS_FIELD);
+ }
+
+ /**
+ * Returns latency of first SetSchema action in milliseconds.
+ *
+ * <p>If all schema fields are backward compatible, the schema will be successful set to Icing.
+ * Otherwise, we will retrieve incompatible types here.
+ *
+ * <p>Please see {@link SetSchemaRequest} for what is "incompatible".
+ */
+ public int getFirstSetSchemaLatencyMillis() {
+ return mBundle.getInt(FIRST_SET_SCHEMA_LATENCY_MILLIS_FIELD);
+ }
+
+ /** Returns whether the first SetSchema action success. */
+ public boolean isFirstSetSchemaSuccess() {
+ return mBundle.getBoolean(IS_FIRST_SET_SCHEMA_SUCCESS_FIELD);
+ }
+
+ /**
+ * Returns latency of second SetSchema action in milliseconds.
+ *
+ * <p>If all schema fields are backward compatible, the schema will be successful set to Icing
+ * in the first setSchema action and this value will be 0. Otherwise, schema types will be set
+ * to Icing by this action.
+ */
+ public int getSecondSetSchemaLatencyMillis() {
+ return mBundle.getInt(SECOND_SET_SCHEMA_LATENCY_MILLIS_FIELD);
+ }
+
+ /** Returns latency of putting migrated document to Icing lib in milliseconds. */
+ public int getSaveDocumentLatencyMillis() {
+ return mBundle.getInt(SAVE_DOCUMENT_LATENCY_MILLIS_FIELD);
+ }
+
+ /** Returns number of document that need to be migrated to another version. */
+ public int getTotalNeedMigratedDocumentCount() {
+ return mBundle.getInt(TOTAL_NEED_MIGRATED_DOCUMENT_COUNT_FIELD);
+ }
+
+ /** Returns number of {@link android.app.appsearch.SetSchemaResponse.MigrationFailure}. */
+ public int getMigrationFailureCount() {
+ return mBundle.getInt(MIGRATION_FAILURE_COUNT_FIELD);
+ }
+
+ /** Returns number of successfully migrated and saved in Icing. */
+ public int getTotalSuccessMigratedDocumentCount() {
+ return mBundle.getInt(TOTAL_SUCCESS_MIGRATED_DOCUMENT_COUNT_FIELD);
+ }
+
+ /** Builder for {@link SchemaMigrationStats}. */
+ public static class Builder {
+
+ private final Bundle mBundle;
+
+ /** Creates a {@link SchemaMigrationStats.Builder}. */
+ public Builder(@NonNull String packageName, @NonNull String database) {
+ mBundle = new Bundle();
+ mBundle.putString(PACKAGE_NAME_FIELD, packageName);
+ mBundle.putString(DATABASE_FIELD, database);
+ }
+
+ /**
+ * Creates a {@link SchemaMigrationStats.Builder} from a given {@link SchemaMigrationStats}.
+ *
+ * <p>The returned builder is a deep copy whose data is separate from this
+ * SchemaMigrationStats.
+ */
+ public Builder(@NonNull SchemaMigrationStats stats) {
+ mBundle = BundleUtil.deepCopy(stats.mBundle);
+ }
+
+ /**
+ * Creates a new {@link SchemaMigrationStats.Builder} from the given Bundle
+ *
+ * <p>The bundle is NOT copied.
+ */
+ public Builder(@NonNull Bundle bundle) {
+ mBundle = Objects.requireNonNull(bundle);
+ }
+
+ /** Sets status code for the schema migration action. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+ mBundle.putInt(STATUS_CODE_FIELD, statusCode);
+ return this;
+ }
+
+ /** Sets the latency for waiting the executor. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setExecutorAcquisitionLatencyMillis(int executorAcquisitionLatencyMillis) {
+ mBundle.putInt(EXECUTOR_ACQUISITION_MILLIS_FIELD, executorAcquisitionLatencyMillis);
+ return this;
+ }
+
+ /** Sets total latency for the schema migration action in milliseconds. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+ mBundle.putInt(TOTAL_LATENCY_MILLIS_FIELD, totalLatencyMillis);
+ return this;
+ }
+
+ /** Sets latency for the GetSchema action in milliseconds. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setGetSchemaLatencyMillis(int getSchemaLatencyMillis) {
+ mBundle.putInt(GET_SCHEMA_LATENCY_MILLIS_FIELD, getSchemaLatencyMillis);
+ return this;
+ }
+
+ /**
+ * Sets latency for querying all documents that need to be migrated to new version and
+ * transforming documents to new version in milliseconds.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setQueryAndTransformLatencyMillis(int queryAndTransformLatencyMillis) {
+ mBundle.putInt(
+ QUERY_AND_TRANSFORM_LATENCY_MILLIS_FIELD, queryAndTransformLatencyMillis);
+ return this;
+ }
+
+ /** Sets latency of first SetSchema action in milliseconds. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setFirstSetSchemaLatencyMillis(int firstSetSchemaLatencyMillis) {
+ mBundle.putInt(FIRST_SET_SCHEMA_LATENCY_MILLIS_FIELD, firstSetSchemaLatencyMillis);
+ return this;
+ }
+
+ /** Returns status of the first SetSchema action. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setIsFirstSetSchemaSuccess(boolean isFirstSetSchemaSuccess) {
+ mBundle.putBoolean(IS_FIRST_SET_SCHEMA_SUCCESS_FIELD, isFirstSetSchemaSuccess);
+ return this;
+ }
+
+ /** Sets latency of second SetSchema action in milliseconds. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setSecondSetSchemaLatencyMillis(int secondSetSchemaLatencyMillis) {
+ mBundle.putInt(SECOND_SET_SCHEMA_LATENCY_MILLIS_FIELD, secondSetSchemaLatencyMillis);
+ return this;
+ }
+
+ /** Sets latency for putting migrated document to Icing lib in milliseconds. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setSaveDocumentLatencyMillis(int saveDocumentLatencyMillis) {
+ mBundle.putInt(SAVE_DOCUMENT_LATENCY_MILLIS_FIELD, saveDocumentLatencyMillis);
+ return this;
+ }
+
+ /** Sets number of document that need to be migrated to another version. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTotalNeedMigratedDocumentCount(int migratedDocumentCount) {
+ mBundle.putInt(TOTAL_NEED_MIGRATED_DOCUMENT_COUNT_FIELD, migratedDocumentCount);
+ return this;
+ }
+
+ /** Sets total document count of successfully migrated and saved in Icing. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTotalSuccessMigratedDocumentCount(int totalSuccessMigratedDocumentCount) {
+ mBundle.putInt(
+ TOTAL_SUCCESS_MIGRATED_DOCUMENT_COUNT_FIELD, totalSuccessMigratedDocumentCount);
+ return this;
+ }
+
+ /** Sets number of {@link android.app.appsearch.SetSchemaResponse.MigrationFailure}. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setMigrationFailureCount(int migrationFailureCount) {
+ mBundle.putInt(MIGRATION_FAILURE_COUNT_FIELD, migrationFailureCount);
+ return this;
+ }
+
+ /**
+ * Builds a new {@link SchemaMigrationStats} from the {@link SchemaMigrationStats.Builder}.
+ */
+ @NonNull
+ public SchemaMigrationStats build() {
+ return new SchemaMigrationStats(mBundle);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/util/BundleUtil.java b/android-34/android/app/appsearch/util/BundleUtil.java
new file mode 100644
index 0000000..52df5b1
--- /dev/null
+++ b/android-34/android/app/appsearch/util/BundleUtil.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 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 android.app.appsearch.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Utilities for working with {@link android.os.Bundle}.
+ *
+ * @hide
+ */
+public final class BundleUtil {
+ private BundleUtil() {}
+
+ /**
+ * Deeply checks two bundles are equal or not.
+ *
+ * <p>Two bundles will be considered equal if they contain the same keys, and each value is also
+ * equal. Bundle values are compared using deepEquals.
+ */
+ @SuppressWarnings("deprecation")
+ public static boolean deepEquals(@Nullable Bundle one, @Nullable Bundle two) {
+ if (one == null && two == null) {
+ return true;
+ }
+ if (one == null || two == null) {
+ return false;
+ }
+ if (one.size() != two.size()) {
+ return false;
+ }
+ if (!one.keySet().equals(two.keySet())) {
+ return false;
+ }
+ // Bundle inherit its equals() from Object.java, which only compare their memory address.
+ // We should iterate all keys and check their presents and values in both bundle.
+ for (String key : one.keySet()) {
+ if (!bundleValueEquals(one.get(key), two.get(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Deeply checks whether two values in a Bundle are equal or not.
+ *
+ * <p>Values of type Bundle are compared using {@link #deepEquals}.
+ */
+ private static boolean bundleValueEquals(@Nullable Object one, @Nullable Object two) {
+ if (one == null && two == null) {
+ return true;
+ }
+ if (one == null || two == null) {
+ return false;
+ }
+ if (one.equals(two)) {
+ return true;
+ }
+ if (one instanceof Bundle && two instanceof Bundle) {
+ return deepEquals((Bundle) one, (Bundle) two);
+ } else if (one instanceof int[] && two instanceof int[]) {
+ return Arrays.equals((int[]) one, (int[]) two);
+ } else if (one instanceof byte[] && two instanceof byte[]) {
+ return Arrays.equals((byte[]) one, (byte[]) two);
+ } else if (one instanceof char[] && two instanceof char[]) {
+ return Arrays.equals((char[]) one, (char[]) two);
+ } else if (one instanceof long[] && two instanceof long[]) {
+ return Arrays.equals((long[]) one, (long[]) two);
+ } else if (one instanceof float[] && two instanceof float[]) {
+ return Arrays.equals((float[]) one, (float[]) two);
+ } else if (one instanceof short[] && two instanceof short[]) {
+ return Arrays.equals((short[]) one, (short[]) two);
+ } else if (one instanceof double[] && two instanceof double[]) {
+ return Arrays.equals((double[]) one, (double[]) two);
+ } else if (one instanceof boolean[] && two instanceof boolean[]) {
+ return Arrays.equals((boolean[]) one, (boolean[]) two);
+ } else if (one instanceof Object[] && two instanceof Object[]) {
+ Object[] arrayOne = (Object[]) one;
+ Object[] arrayTwo = (Object[]) two;
+ if (arrayOne.length != arrayTwo.length) {
+ return false;
+ }
+ if (Arrays.equals(arrayOne, arrayTwo)) {
+ return true;
+ }
+ for (int i = 0; i < arrayOne.length; i++) {
+ if (!bundleValueEquals(arrayOne[i], arrayTwo[i])) {
+ return false;
+ }
+ }
+ return true;
+ } else if (one instanceof ArrayList && two instanceof ArrayList) {
+ ArrayList<?> listOne = (ArrayList<?>) one;
+ ArrayList<?> listTwo = (ArrayList<?>) two;
+ if (listOne.size() != listTwo.size()) {
+ return false;
+ }
+ for (int i = 0; i < listOne.size(); i++) {
+ if (!bundleValueEquals(listOne.get(i), listTwo.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ } else if (one instanceof SparseArray && two instanceof SparseArray) {
+ SparseArray<?> arrayOne = (SparseArray<?>) one;
+ SparseArray<?> arrayTwo = (SparseArray<?>) two;
+ if (arrayOne.size() != arrayTwo.size()) {
+ return false;
+ }
+ for (int i = 0; i < arrayOne.size(); i++) {
+ if (arrayOne.keyAt(i) != arrayTwo.keyAt(i)
+ || !bundleValueEquals(arrayOne.valueAt(i), arrayTwo.valueAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Calculates the hash code for a bundle.
+ *
+ * <p>The hash code is only effected by the contents in the bundle. Bundles will get consistent
+ * hash code if they have same contents.
+ */
+ @SuppressWarnings("deprecation")
+ public static int deepHashCode(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return 0;
+ }
+ int[] hashCodes = new int[bundle.size() + 1];
+ int hashCodeIdx = 0;
+ // Bundle inherit its hashCode() from Object.java, which only relative to their memory
+ // address. Bundle doesn't have an order, so we should iterate all keys and combine
+ // their value's hashcode into an array. And use the hashcode of the array to be
+ // the hashcode of the bundle.
+ // Because bundle.keySet() doesn't guarantee any particular order, we need to sort the keys
+ // in case the iteration order varies from run to run.
+ String[] keys = bundle.keySet().toArray(new String[0]);
+ Arrays.sort(keys);
+ // Hash the keys so we can detect key-only differences
+ hashCodes[hashCodeIdx++] = Arrays.hashCode(keys);
+ for (int keyIdx = 0; keyIdx < keys.length; keyIdx++) {
+ Object value = bundle.get(keys[keyIdx]);
+ if (value instanceof Bundle) {
+ hashCodes[hashCodeIdx++] = deepHashCode((Bundle) value);
+ } else if (value instanceof int[]) {
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((int[]) value);
+ } else if (value instanceof byte[]) {
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((byte[]) value);
+ } else if (value instanceof char[]) {
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((char[]) value);
+ } else if (value instanceof long[]) {
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((long[]) value);
+ } else if (value instanceof float[]) {
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((float[]) value);
+ } else if (value instanceof short[]) {
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((short[]) value);
+ } else if (value instanceof double[]) {
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((double[]) value);
+ } else if (value instanceof boolean[]) {
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((boolean[]) value);
+ } else if (value instanceof String[]) {
+ // Optimization to avoid Object[] handler creating an inner array for common cases
+ hashCodes[hashCodeIdx++] = Arrays.hashCode((String[]) value);
+ } else if (value instanceof Object[]) {
+ Object[] array = (Object[]) value;
+ int[] innerHashCodes = new int[array.length];
+ for (int j = 0; j < array.length; j++) {
+ if (array[j] instanceof Bundle) {
+ innerHashCodes[j] = deepHashCode((Bundle) array[j]);
+ } else if (array[j] != null) {
+ innerHashCodes[j] = array[j].hashCode();
+ }
+ }
+ hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
+ } else if (value instanceof ArrayList) {
+ ArrayList<?> list = (ArrayList<?>) value;
+ int[] innerHashCodes = new int[list.size()];
+ for (int j = 0; j < innerHashCodes.length; j++) {
+ Object item = list.get(j);
+ if (item instanceof Bundle) {
+ innerHashCodes[j] = deepHashCode((Bundle) item);
+ } else if (item != null) {
+ innerHashCodes[j] = item.hashCode();
+ }
+ }
+ hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
+ } else if (value instanceof SparseArray) {
+ SparseArray<?> array = (SparseArray<?>) value;
+ int[] innerHashCodes = new int[array.size() * 2];
+ for (int j = 0; j < array.size(); j++) {
+ innerHashCodes[j * 2] = array.keyAt(j);
+ Object item = array.valueAt(j);
+ if (item instanceof Bundle) {
+ innerHashCodes[j * 2 + 1] = deepHashCode((Bundle) item);
+ } else if (item != null) {
+ innerHashCodes[j * 2 + 1] = item.hashCode();
+ }
+ }
+ hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
+ } else if (value != null) {
+ hashCodes[hashCodeIdx++] = value.hashCode();
+ } else {
+ hashCodes[hashCodeIdx++] = 0;
+ }
+ }
+ return Arrays.hashCode(hashCodes);
+ }
+
+ /**
+ * Deeply clones a Bundle.
+ *
+ * <p>Values which are Bundles, Lists or Arrays are deeply copied themselves.
+ */
+ @NonNull
+ public static Bundle deepCopy(@NonNull Bundle bundle) {
+ // Write bundle to bytes
+ Parcel parcel = Parcel.obtain();
+ try {
+ parcel.writeBundle(bundle);
+ byte[] serializedMessage = parcel.marshall();
+
+ // Read bundle from bytes
+ parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
+ parcel.setDataPosition(0);
+ return parcel.readBundle();
+ } finally {
+ parcel.recycle();
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/util/DocumentIdUtil.java b/android-34/android/app/appsearch/util/DocumentIdUtil.java
new file mode 100644
index 0000000..749f5cc
--- /dev/null
+++ b/android-34/android/app/appsearch/util/DocumentIdUtil.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.util;
+
+import android.annotation.NonNull;
+import android.app.appsearch.GenericDocument;
+
+import java.util.Objects;
+
+/** A util class with methods for working with document ids. */
+public class DocumentIdUtil {
+ private DocumentIdUtil() {}
+
+ /** A delimiter between the namespace and the document id. */
+ private static final String NAMESPACE_DELIMITER = "#";
+
+ /**
+ * In regex, 4 backslashes in Java represent a single backslash in Regex. This will escape the
+ * namespace delimiter.
+ */
+ private static final String NAMESPACE_DELIMITER_REPLACEMENT_REGEX = "\\\\#";
+
+ /**
+ * Generates a qualified id based on package, database, and a {@link GenericDocument}.
+ *
+ * @param packageName The package the document belongs to.
+ * @param databaseName The database containing the document.
+ * @param document The document to generate a qualified id for.
+ * @return the qualified id of a document.
+ * @see #createQualifiedId(String, String, String, String)
+ */
+ @NonNull
+ public static String createQualifiedId(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull GenericDocument document) {
+ return createQualifiedId(
+ packageName, databaseName, document.getNamespace(), document.getId());
+ }
+
+ /**
+ * Generates a qualified id based on package, database, namespace, and doc id.
+ *
+ * <p>A qualified id is a String referring to the combined package name, database name,
+ * namespace, and id of the document. It is useful for linking one document to another in order
+ * to perform a join operation.
+ *
+ * @param packageName The package the document belongs to.
+ * @param databaseName The database containing the document.
+ * @param namespace The namespace of the document.
+ * @param id The id of the document.
+ * @return the qualified id of a document
+ */
+ // TODO(b/256022027): Add @link to QUALIFIED_ID and JoinSpec
+ @NonNull
+ public static String createQualifiedId(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull String namespace,
+ @NonNull String id) {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(databaseName);
+ Objects.requireNonNull(namespace);
+ Objects.requireNonNull(id);
+
+ StringBuilder qualifiedId = new StringBuilder(escapeNsDelimiters(packageName));
+
+ qualifiedId
+ .append('$')
+ .append(escapeNsDelimiters(databaseName))
+ .append('/')
+ .append(escapeNsDelimiters(namespace))
+ .append(NAMESPACE_DELIMITER)
+ .append(escapeNsDelimiters(id));
+ return qualifiedId.toString();
+ }
+
+ /**
+ * Escapes both the namespace delimiter and backslashes.
+ *
+ * <p>For example, say the raw namespace contains ...\#... . if we only escape the namespace
+ * delimiter, we would get ...\\#..., which would appear to be a delimiter, and split the
+ * namespace in two. We need to escape the backslash as well, resulting in ...\\\#..., which is
+ * not a delimiter, keeping the namespace together.
+ *
+ * @param original The String to escape
+ * @return An escaped string
+ */
+ private static String escapeNsDelimiters(@NonNull String original) {
+ // Four backslashes represent a single backslash in regex.
+ return original.replaceAll("\\\\", "\\\\\\\\")
+ .replaceAll(NAMESPACE_DELIMITER, NAMESPACE_DELIMITER_REPLACEMENT_REGEX);
+ }
+}
diff --git a/android-34/android/app/appsearch/util/IndentingStringBuilder.java b/android-34/android/app/appsearch/util/IndentingStringBuilder.java
new file mode 100644
index 0000000..7531ce4
--- /dev/null
+++ b/android-34/android/app/appsearch/util/IndentingStringBuilder.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.util;
+
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+
+/**
+ * Utility for building indented strings.
+ *
+ * <p>This is a wrapper for {@link StringBuilder} for appending strings with indentation. The
+ * indentation level can be increased by calling {@link #increaseIndentLevel()} and decreased by
+ * calling {@link #decreaseIndentLevel()}.
+ *
+ * <p>Indentation is applied after each newline character for the given indent level.
+ *
+ * @hide
+ */
+public class IndentingStringBuilder {
+ private final StringBuilder mStringBuilder = new StringBuilder();
+
+ // Indicates whether next non-newline character should have an indent applied before it.
+ private boolean mIndentNext = false;
+ private int mIndentLevel = 0;
+
+ /** Increases the indent level by one for appended strings. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public IndentingStringBuilder increaseIndentLevel() {
+ mIndentLevel++;
+ return this;
+ }
+
+ /** Decreases the indent level by one for appended strings. */
+ @CanIgnoreReturnValue
+ @NonNull
+ public IndentingStringBuilder decreaseIndentLevel() throws IllegalStateException {
+ if (mIndentLevel == 0) {
+ throw new IllegalStateException("Cannot set indent level below 0.");
+ }
+ mIndentLevel--;
+ return this;
+ }
+
+ /**
+ * Appends provided {@code String} at the current indentation level.
+ *
+ * <p>Indentation is applied after each newline character.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public IndentingStringBuilder append(@NonNull String str) {
+ applyIndentToString(str);
+ return this;
+ }
+
+ /**
+ * Appends provided {@code Object}, represented as a {@code String}, at the current indentation
+ * level.
+ *
+ * <p>Indentation is applied after each newline character.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public IndentingStringBuilder append(@NonNull Object obj) {
+ applyIndentToString(obj.toString());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return mStringBuilder.toString();
+ }
+
+ /** Adds indent string to the {@link StringBuilder} instance for current indent level. */
+ private void applyIndent() {
+ for (int i = 0; i < mIndentLevel; i++) {
+ mStringBuilder.append(" ");
+ }
+ }
+
+ /**
+ * Applies indent, for current indent level, after each newline character.
+ *
+ * <p>Consecutive newline characters are not indented.
+ */
+ private void applyIndentToString(@NonNull String str) {
+ int index = str.indexOf("\n");
+ if (index == 0) {
+ // String begins with new line character: append newline and slide past newline.
+ mStringBuilder.append("\n");
+ mIndentNext = true;
+ if (str.length() > 1) {
+ applyIndentToString(str.substring(index + 1));
+ }
+ } else if (index >= 1) {
+ // String contains new line character: divide string between newline, append new line,
+ // and recurse on each string.
+ String beforeIndentString = str.substring(0, index);
+ applyIndentToString(beforeIndentString);
+ mStringBuilder.append("\n");
+ mIndentNext = true;
+ if (str.length() > index + 1) {
+ String afterIndentString = str.substring(index + 1);
+ applyIndentToString(afterIndentString);
+ }
+ } else {
+ // String does not contain newline character: append string.
+ if (mIndentNext) {
+ applyIndent();
+ mIndentNext = false;
+ }
+ mStringBuilder.append(str);
+ }
+ }
+}
diff --git a/android-34/android/app/appsearch/util/LogUtil.java b/android-34/android/app/appsearch/util/LogUtil.java
new file mode 100644
index 0000000..7ca7865
--- /dev/null
+++ b/android-34/android/app/appsearch/util/LogUtil.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.util.Log;
+
+/**
+ * Utilities for logging to logcat.
+ *
+ * @hide
+ */
+public final class LogUtil {
+ /** Whether to log {@link Log#VERBOSE} and {@link Log#DEBUG} logs. */
+ // TODO(b/232285376): If it becomes possible to detect an eng build, turn this on by default
+ // for eng builds.
+ public static final boolean DEBUG = false;
+
+ /**
+ * The {@link #piiTrace} logs are intended for sensitive data that can't be enabled in
+ * production, so they are build-gated by this constant.
+ *
+ * <p>
+ *
+ * <ul>
+ * <li>0: no tracing.
+ * <li>1: fast tracing (statuses/counts only)
+ * <li>2: full tracing (complete messages)
+ * </ul>
+ */
+ private static final int PII_TRACE_LEVEL = 0;
+
+ private LogUtil() {}
+
+ /** Returns whether piiTrace() is enabled (PII_TRACE_LEVEL > 0). */
+ public static boolean isPiiTraceEnabled() {
+ return PII_TRACE_LEVEL > 0;
+ }
+
+ /**
+ * If icing lib interaction tracing is enabled via {@link #PII_TRACE_LEVEL}, logs the provided
+ * message to logcat.
+ *
+ * <p>If {@link #PII_TRACE_LEVEL} is 0, nothing is logged and this method returns immediately.
+ */
+ public static void piiTrace(
+ @Size(min = 0, max = 23) @NonNull String tag, @NonNull String message) {
+ piiTrace(tag, message, /*fastTraceObj=*/ null, /*fullTraceObj=*/ null);
+ }
+
+ /**
+ * If icing lib interaction tracing is enabled via {@link #PII_TRACE_LEVEL}, logs the provided
+ * message and object to logcat.
+ *
+ * <p>If {@link #PII_TRACE_LEVEL} is 0, nothing is logged and this method returns immediately.
+ *
+ * <p>Otherwise, {@code traceObj} is logged if it is non-null.
+ */
+ public static void piiTrace(
+ @Size(min = 0, max = 23) @NonNull String tag,
+ @NonNull String message,
+ @Nullable Object traceObj) {
+ piiTrace(tag, message, /*fastTraceObj=*/ traceObj, /*fullTraceObj=*/ null);
+ }
+
+ /**
+ * If icing lib interaction tracing is enabled via {@link #PII_TRACE_LEVEL}, logs the provided
+ * message and objects to logcat.
+ *
+ * <p>If {@link #PII_TRACE_LEVEL} is 0, nothing is logged and this method returns immediately.
+ *
+ * <p>If {@link #PII_TRACE_LEVEL} is 1, {@code fastTraceObj} is logged if it is non-null.
+ *
+ * <p>If {@link #PII_TRACE_LEVEL} is 2, {@code fullTraceObj} is logged if it is non-null, else
+ * {@code fastTraceObj} is logged if it is non-null..
+ */
+ public static void piiTrace(
+ @Size(min = 0, max = 23) @NonNull String tag,
+ @NonNull String message,
+ @Nullable Object fastTraceObj,
+ @Nullable Object fullTraceObj) {
+ if (PII_TRACE_LEVEL == 0) {
+ return;
+ }
+ StringBuilder builder = new StringBuilder("(trace) ").append(message);
+ if (PII_TRACE_LEVEL == 1 && fastTraceObj != null) {
+ builder.append(": ").append(fastTraceObj);
+ } else if (PII_TRACE_LEVEL == 2 && fullTraceObj != null) {
+ builder.append(": ").append(fullTraceObj);
+ } else if (PII_TRACE_LEVEL == 2 && fastTraceObj != null) {
+ builder.append(": ").append(fastTraceObj);
+ }
+ Log.i(tag, builder.toString());
+ }
+}
diff --git a/android-34/android/app/appsearch/util/SchemaMigrationUtil.java b/android-34/android/app/appsearch/util/SchemaMigrationUtil.java
new file mode 100644
index 0000000..fcacb5f
--- /dev/null
+++ b/android-34/android/app/appsearch/util/SchemaMigrationUtil.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.util;
+
+import static android.app.appsearch.AppSearchResult.RESULT_INVALID_SCHEMA;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.InternalSetSchemaResponse;
+import android.app.appsearch.Migrator;
+import android.app.appsearch.SetSchemaResponse;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utilities for schema migration.
+ *
+ * @hide
+ */
+public final class SchemaMigrationUtil {
+ private SchemaMigrationUtil() {}
+
+ /**
+ * Returns all active {@link Migrator}s that need to be triggered in this migration.
+ *
+ * <p>{@link Migrator#shouldMigrate} returns {@code true} will make the {@link Migrator} active.
+ */
+ @NonNull
+ public static Map<String, Migrator> getActiveMigrators(
+ @NonNull Set<AppSearchSchema> existingSchemas,
+ @NonNull Map<String, Migrator> migrators,
+ int currentVersion,
+ int finalVersion) {
+ if (currentVersion == finalVersion) {
+ return Collections.emptyMap();
+ }
+ Set<String> existingTypes = new ArraySet<>(existingSchemas.size());
+ for (AppSearchSchema schema : existingSchemas) {
+ existingTypes.add(schema.getSchemaType());
+ }
+
+ Map<String, Migrator> activeMigrators = new ArrayMap<>();
+ for (Map.Entry<String, Migrator> entry : migrators.entrySet()) {
+ // The device contains the source type, and we should trigger migration for the type.
+ String schemaType = entry.getKey();
+ Migrator migrator = entry.getValue();
+ if (existingTypes.contains(schemaType)
+ && migrator.shouldMigrate(currentVersion, finalVersion)) {
+ activeMigrators.put(schemaType, migrator);
+ }
+ }
+ return activeMigrators;
+ }
+
+ /**
+ * Checks the setSchema() call won't delete any types or has incompatible types after all {@link
+ * Migrator} has been triggered.
+ */
+ public static void checkDeletedAndIncompatibleAfterMigration(
+ @NonNull InternalSetSchemaResponse internalSetSchemaResponse,
+ @NonNull Set<String> activeMigrators)
+ throws AppSearchException {
+ if (internalSetSchemaResponse.isSuccess()) {
+ return;
+ }
+ SetSchemaResponse setSchemaResponse = internalSetSchemaResponse.getSetSchemaResponse();
+ Set<String> unmigratedIncompatibleTypes =
+ new ArraySet<>(setSchemaResponse.getIncompatibleTypes());
+ unmigratedIncompatibleTypes.removeAll(activeMigrators);
+
+ Set<String> unmigratedDeletedTypes = new ArraySet<>(setSchemaResponse.getDeletedTypes());
+ unmigratedDeletedTypes.removeAll(activeMigrators);
+
+ // check if there are any unmigrated incompatible types or deleted types. If there
+ // are, we will throw an exception. That's the only case we swallowed in the
+ // AppSearchImpl#setSchema().
+ // Since the force override is false, the schema will not have been set if there are
+ // any incompatible or deleted types.
+ if (!unmigratedIncompatibleTypes.isEmpty() || !unmigratedDeletedTypes.isEmpty()) {
+ throw new AppSearchException(
+ RESULT_INVALID_SCHEMA, internalSetSchemaResponse.getErrorMessage());
+ }
+ }
+}
diff --git a/android-34/android/app/backup/BackupUtilsTest.java b/android-34/android/app/backup/BackupUtilsTest.java
deleted file mode 100644
index 099cde0..0000000
--- a/android-34/android/app/backup/BackupUtilsTest.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package android.app.backup;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
-import android.content.Context;
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.internal.DoNotInstrument;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-@RunWith(RobolectricTestRunner.class)
-@Presubmit
-@DoNotInstrument
-public class BackupUtilsTest {
- private Context mContext;
-
- @Before
- public void setUp() throws Exception {
- mContext = RuntimeEnvironment.application;
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenFileAndPathListHasIt() throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(file("a/b.txt"), paths(file("a/b.txt")));
-
- assertThat(isSpecified).isTrue();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenFileAndPathListHasItsDirectory()
- throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(file("a/b.txt"), paths(directory("a")));
-
- assertThat(isSpecified).isTrue();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenFileAndPathListHasOtherFile() throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(file("a/b.txt"), paths(file("a/c.txt")));
-
- assertThat(isSpecified).isFalse();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenFileAndPathListEmpty() throws Exception {
- boolean isSpecified = BackupUtils.isFileSpecifiedInPathList(file("a/b.txt"), paths());
-
- assertThat(isSpecified).isFalse();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenDirectoryAndPathListHasIt() throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(directory("a"), paths(directory("a")));
-
- assertThat(isSpecified).isTrue();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenDirectoryAndPathListEmpty() throws Exception {
- boolean isSpecified = BackupUtils.isFileSpecifiedInPathList(directory("a"), paths());
-
- assertThat(isSpecified).isFalse();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenDirectoryAndPathListHasParent() throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(directory("a/b"), paths(directory("a")));
-
- assertThat(isSpecified).isFalse();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenFileAndPathListDoesntContainDirectory()
- throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(file("a/b.txt"), paths(directory("c")));
-
- assertThat(isSpecified).isFalse();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenFileAndPathListHasDirectoryWhoseNameIsPrefix()
- throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(file("a/b.txt"), paths(directory("a/b")));
-
- assertThat(isSpecified).isFalse();
- }
-
- @Test
- public void testIsFileSpecifiedInPathList_whenFileAndPathListHasDirectoryWhoseNameIsPrefix2()
- throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(
- file("name/subname.txt"), paths(directory("nam")));
-
- assertThat(isSpecified).isFalse();
- }
-
- @Test
- public void
- testIsFileSpecifiedInPathList_whenFileAndPathListContainsFirstNotRelatedAndSecondContainingDirectory()
- throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(
- file("a/b.txt"), paths(directory("b"), directory("a")));
-
- assertThat(isSpecified).isTrue();
- }
-
- @Test
- public void
- testIsFileSpecifiedInPathList_whenDirectoryAndPathListContainsFirstNotRelatedAndSecondSameDirectory()
- throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(
- directory("a/b"), paths(directory("b"), directory("a/b")));
-
- assertThat(isSpecified).isTrue();
- }
-
- @Test
- public void
- testIsFileSpecifiedInPathList_whenFileAndPathListContainsFirstNotRelatedFileAndSecondSameFile()
- throws Exception {
- boolean isSpecified =
- BackupUtils.isFileSpecifiedInPathList(
- file("a/b.txt"), paths(directory("b"), file("a/b.txt")));
-
- assertThat(isSpecified).isTrue();
- }
-
- private File file(String path) throws IOException {
- File file = new File(mContext.getDataDir(), path);
- File parent = file.getParentFile();
- parent.mkdirs();
- file.createNewFile();
- if (!file.isFile()) {
- throw new IOException("Couldn't create file");
- }
- return file;
- }
-
- private File directory(String path) throws IOException {
- File directory = new File(mContext.getDataDir(), path);
- directory.mkdirs();
- if (!directory.isDirectory()) {
- throw new IOException("Couldn't create directory");
- }
- return directory;
- }
-
- private Collection<PathWithRequiredFlags> paths(File... files) {
- return Stream.of(files)
- .map(file -> new PathWithRequiredFlags(file.getPath(), 0))
- .collect(Collectors.toList());
- }
-}
diff --git a/android-34/android/app/backup/ForwardingBackupAgent.java b/android-34/android/app/backup/ForwardingBackupAgent.java
deleted file mode 100644
index 4ff5b7c..0000000
--- a/android-34/android/app/backup/ForwardingBackupAgent.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package android.app.backup;
-
-import android.content.Context;
-import android.os.ParcelFileDescriptor;
-
-import java.io.File;
-import java.io.IOException;
-
-/**
- * Useful for spying in {@link BackupAgent} instances since their {@link BackupAgent#onBind()} is
- * final and always points to the original instance, instead of the spy.
- *
- * <p>To use, construct a spy of the desired {@link BackupAgent}, spying on the methods of interest.
- * Then, where you need to pass the agent, use {@link ForwardingBackupAgent#forward(BackupAgent)}
- * with the spy.
- */
-public class ForwardingBackupAgent extends BackupAgent {
- /** Returns a {@link BackupAgent} that forwards method calls to {@code backupAgent}. */
- public static BackupAgent forward(BackupAgent backupAgent) {
- return new ForwardingBackupAgent(backupAgent);
- }
-
- private final BackupAgent mBackupAgent;
-
- private ForwardingBackupAgent(BackupAgent backupAgent) {
- mBackupAgent = backupAgent;
- }
-
- @Override
- public void onCreate() {
- mBackupAgent.onCreate();
- }
-
- @Override
- public void onDestroy() {
- mBackupAgent.onDestroy();
- }
-
- @Override
- public void onBackup(
- ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)
- throws IOException {
- mBackupAgent.onBackup(oldState, data, newState);
- }
-
- @Override
- public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
- throws IOException {
- mBackupAgent.onRestore(data, appVersionCode, newState);
- }
-
- @Override
- public void onRestore(BackupDataInput data, long appVersionCode, ParcelFileDescriptor newState)
- throws IOException {
- mBackupAgent.onRestore(data, appVersionCode, newState);
- }
-
- @Override
- public void onFullBackup(FullBackupDataOutput data) throws IOException {
- mBackupAgent.onFullBackup(data);
- }
-
- @Override
- public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
- mBackupAgent.onQuotaExceeded(backupDataBytes, quotaBytes);
- }
-
- @Override
- public void onRestoreFile(
- ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime)
- throws IOException {
- mBackupAgent.onRestoreFile(data, size, destination, type, mode, mtime);
- }
-
- @Override
- protected void onRestoreFile(
- ParcelFileDescriptor data,
- long size,
- int type,
- String domain,
- String path,
- long mode,
- long mtime)
- throws IOException {
- mBackupAgent.onRestoreFile(data, size, type, domain, path, mode, mtime);
- }
-
- @Override
- public void onRestoreFinished() {
- mBackupAgent.onRestoreFinished();
- }
-
- @Override
- public void attach(Context context) {
- mBackupAgent.attach(context);
- }
-}
diff --git a/android-34/android/app/ondevicepersonalization/OnDevicePersonalizationSystemServiceManager.java b/android-34/android/app/ondevicepersonalization/OnDevicePersonalizationSystemServiceManager.java
new file mode 100644
index 0000000..5c5ecd7
--- /dev/null
+++ b/android-34/android/app/ondevicepersonalization/OnDevicePersonalizationSystemServiceManager.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.ondevicepersonalization;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+
+/**
+ * API for ODP module to interact with ODP System Server Service.
+ * @hide
+ */
+public class OnDevicePersonalizationSystemServiceManager {
+ /** Identifier for the ODP binder service in system_server. */
+ public static final String ON_DEVICE_PERSONALIZATION_SYSTEM_SERVICE =
+ "ondevicepersonalization_system_service";
+
+ @NonNull private final IOnDevicePersonalizationSystemService mService;
+
+ /** @hide */
+ public OnDevicePersonalizationSystemServiceManager(@NonNull IBinder service) {
+ mService = IOnDevicePersonalizationSystemService.Stub.asInterface(service);
+ }
+
+ /** @hide */
+ @NonNull public IOnDevicePersonalizationSystemService getService() {
+ return mService;
+ }
+}
diff --git a/android-34/android/app/role/OnRoleHoldersChangedListener.java b/android-34/android/app/role/OnRoleHoldersChangedListener.java
new file mode 100644
index 0000000..5958deb
--- /dev/null
+++ b/android-34/android/app/role/OnRoleHoldersChangedListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.role;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.UserHandle;
+
+/**
+ * Listener for role holder changes.
+ *
+ * @hide
+ */
+@SystemApi
+public interface OnRoleHoldersChangedListener {
+
+ /**
+ * Called when the holders of roles are changed.
+ *
+ * @param roleName the name of the role whose holders are changed
+ * @param user the user for this role holder change
+ */
+ void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user);
+}
diff --git a/android-34/android/app/role/RoleControllerManager.java b/android-34/android/app/role/RoleControllerManager.java
new file mode 100644
index 0000000..3b990b3
--- /dev/null
+++ b/android-34/android/app/role/RoleControllerManager.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.role;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteCallback;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Interface for communicating with the role controller.
+ *
+ * @hide
+ */
+public class RoleControllerManager {
+
+ private static final String LOG_TAG = RoleControllerManager.class.getSimpleName();
+
+ private static final long REQUEST_TIMEOUT_MILLIS = 15 * 1000;
+
+ private static volatile ComponentName sRemoteServiceComponentName;
+
+ private static final Object sRemoteServicesLock = new Object();
+
+ /**
+ * Global remote services (per user) used by all {@link RoleControllerManager managers}.
+ */
+ @GuardedBy("sRemoteServicesLock")
+ private static final SparseArray<ServiceConnector<IRoleController>> sRemoteServices =
+ new SparseArray<>();
+
+ @NonNull
+ private final ServiceConnector<IRoleController> mRemoteService;
+
+ /**
+ * Initialize the remote service component name once so that we can avoid acquiring the
+ * PackageManagerService lock in constructor.
+ *
+ * @see #createWithInitializedRemoteServiceComponentName(Handler, Context)
+ *
+ * @hide
+ */
+ public static void initializeRemoteServiceComponentName(@NonNull Context context) {
+ sRemoteServiceComponentName = getRemoteServiceComponentName(context);
+ }
+
+ /**
+ * Create a {@link RoleControllerManager} instance with the initialized remote service component
+ * name so that we can avoid acquiring the PackageManagerService lock in constructor.
+ *
+ * @see #initializeRemoteServiceComponentName(Context)
+ *
+ * @hide
+ */
+ @NonNull
+ public static RoleControllerManager createWithInitializedRemoteServiceComponentName(
+ @NonNull Handler handler, @NonNull Context context) {
+ return new RoleControllerManager(sRemoteServiceComponentName, handler, context);
+ }
+
+ private RoleControllerManager(@NonNull ComponentName remoteServiceComponentName,
+ @NonNull Handler handler, @NonNull Context context) {
+ synchronized (sRemoteServicesLock) {
+ int userId = context.getUser().getIdentifier();
+ ServiceConnector<IRoleController> remoteService = sRemoteServices.get(userId);
+ if (remoteService == null) {
+ remoteService = new ServiceConnector.Impl<IRoleController>(
+ context.getApplicationContext(),
+ new Intent(RoleControllerService.SERVICE_INTERFACE)
+ .setComponent(remoteServiceComponentName),
+ 0 /* bindingFlags */, userId, IRoleController.Stub::asInterface) {
+
+ @Override
+ protected Handler getJobHandler() {
+ return handler;
+ }
+ };
+ sRemoteServices.put(userId, remoteService);
+ }
+ mRemoteService = remoteService;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public RoleControllerManager(@NonNull Context context) {
+ this(getRemoteServiceComponentName(context), new Handler(Looper.getMainLooper()), context);
+ }
+
+ @NonNull
+ private static ComponentName getRemoteServiceComponentName(@NonNull Context context) {
+ Intent intent = new Intent(RoleControllerService.SERVICE_INTERFACE);
+ PackageManager packageManager = context.getPackageManager();
+ intent.setPackage(packageManager.getPermissionControllerPackageName());
+ ServiceInfo serviceInfo = packageManager.resolveService(intent, 0).serviceInfo;
+ return new ComponentName(serviceInfo.packageName, serviceInfo.name);
+ }
+
+ /**
+ * @see RoleControllerService#onGrantDefaultRoles()
+ *
+ * @hide
+ */
+ public void grantDefaultRoles(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<Boolean> callback) {
+ AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> {
+ AndroidFuture<Bundle> future = new AndroidFuture<>();
+ service.grantDefaultRoles(new RemoteCallback(future::complete));
+ return future;
+ });
+ propagateCallback(operation, "grantDefaultRoles", executor, callback);
+ }
+
+ /**
+ * @see RoleControllerService#onAddRoleHolder(String, String, int)
+ *
+ * @hide
+ */
+ public void onAddRoleHolder(@NonNull String roleName, @NonNull String packageName,
+ @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) {
+ AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> {
+ AndroidFuture<Bundle> future = new AndroidFuture<>();
+ service.onAddRoleHolder(roleName, packageName, flags,
+ new RemoteCallback(future::complete));
+ return future;
+ });
+ propagateCallback(operation, "onAddRoleHolder", callback);
+ }
+
+ /**
+ * @see RoleControllerService#onRemoveRoleHolder(String, String, int)
+ *
+ * @hide
+ */
+ public void onRemoveRoleHolder(@NonNull String roleName, @NonNull String packageName,
+ @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) {
+ AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> {
+ AndroidFuture<Bundle> future = new AndroidFuture<>();
+ service.onRemoveRoleHolder(roleName, packageName, flags,
+ new RemoteCallback(future::complete));
+ return future;
+ });
+ propagateCallback(operation, "onRemoveRoleHolder", callback);
+ }
+
+ /**
+ * @see RoleControllerService#onClearRoleHolders(String, int)
+ *
+ * @hide
+ */
+ public void onClearRoleHolders(@NonNull String roleName,
+ @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) {
+ AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> {
+ AndroidFuture<Bundle> future = new AndroidFuture<>();
+ service.onClearRoleHolders(roleName, flags,
+ new RemoteCallback(future::complete));
+ return future;
+ });
+ propagateCallback(operation, "onClearRoleHolders", callback);
+ }
+
+ /**
+ * @see RoleControllerService#onIsApplicationVisibleForRole(String, String)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ public void isApplicationVisibleForRole(@NonNull String roleName, @NonNull String packageName,
+ @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) {
+ AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> {
+ AndroidFuture<Bundle> future = new AndroidFuture<>();
+ service.isApplicationVisibleForRole(roleName, packageName,
+ new RemoteCallback(future::complete));
+ return future;
+ });
+ propagateCallback(operation, "isApplicationVisibleForRole", executor, callback);
+ }
+
+ /**
+ * @see RoleControllerService#onIsRoleVisible(String)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ public void isRoleVisible(@NonNull String roleName,
+ @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) {
+ AndroidFuture<Bundle> operation = mRemoteService.postAsync(service -> {
+ AndroidFuture<Bundle> future = new AndroidFuture<>();
+ service.isRoleVisible(roleName, new RemoteCallback(future::complete));
+ return future;
+ });
+ propagateCallback(operation, "isRoleVisible", executor, callback);
+ }
+
+ private void propagateCallback(AndroidFuture<Bundle> operation, String opName,
+ @CallbackExecutor @NonNull Executor executor,
+ Consumer<Boolean> destination) {
+ operation.orTimeout(REQUEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+ .whenComplete((res, err) -> executor.execute(() -> {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (err != null) {
+ Log.e(LOG_TAG, "Error calling " + opName + "()", err);
+ destination.accept(false);
+ } else {
+ destination.accept(res != null);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }));
+ }
+
+ private void propagateCallback(AndroidFuture<Bundle> operation, String opName,
+ RemoteCallback destination) {
+ operation.orTimeout(REQUEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+ .whenComplete((res, err) -> {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (err != null) {
+ Log.e(LOG_TAG, "Error calling " + opName + "()", err);
+ destination.sendResult(null);
+ } else {
+ destination.sendResult(res);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ });
+ }
+}
diff --git a/android-34/android/app/role/RoleControllerService.java b/android-34/android/app/role/RoleControllerService.java
new file mode 100644
index 0000000..cf78729
--- /dev/null
+++ b/android-34/android/app/role/RoleControllerService.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.role;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.WorkerThread;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteCallback;
+import android.os.UserHandle;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Abstract base class for the role controller service.
+ * <p>
+ * Subclass should implement the business logic for role management, including enforcing role
+ * requirements and granting or revoking relevant privileges of roles. This class can only be
+ * implemented by the permission controller app which is registered in {@code PackageManager}.
+ *
+ * @deprecated The role controller service is an internal implementation detail inside role, and it
+ * may be replaced by other mechanisms in the future and no longer be called.
+ *
+ * @hide
+ */
+@Deprecated
+@SystemApi
+public abstract class RoleControllerService extends Service {
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ public static final String SERVICE_INTERFACE = "android.app.role.RoleControllerService";
+
+ private HandlerThread mWorkerThread;
+ private Handler mWorkerHandler;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mWorkerThread = new HandlerThread(RoleControllerService.class.getSimpleName());
+ mWorkerThread.start();
+ mWorkerHandler = new Handler(mWorkerThread.getLooper());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ mWorkerThread.quitSafely();
+ }
+
+ @Nullable
+ @Override
+ public final IBinder onBind(@Nullable Intent intent) {
+ return new IRoleController.Stub() {
+
+ @Override
+ public void grantDefaultRoles(RemoteCallback callback) {
+ enforceCallerSystemUid("grantDefaultRoles");
+
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ mWorkerHandler.post(() -> RoleControllerService.this.grantDefaultRoles(callback));
+ }
+
+ @Override
+ public void onAddRoleHolder(String roleName, String packageName, int flags,
+ RemoteCallback callback) {
+ enforceCallerSystemUid("onAddRoleHolder");
+
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName,
+ "packageName cannot be null or empty");
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ mWorkerHandler.post(() -> RoleControllerService.this.onAddRoleHolder(roleName,
+ packageName, flags, callback));
+ }
+
+ @Override
+ public void onRemoveRoleHolder(String roleName, String packageName, int flags,
+ RemoteCallback callback) {
+ enforceCallerSystemUid("onRemoveRoleHolder");
+
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName,
+ "packageName cannot be null or empty");
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ mWorkerHandler.post(() -> RoleControllerService.this.onRemoveRoleHolder(roleName,
+ packageName, flags, callback));
+ }
+
+ @Override
+ public void onClearRoleHolders(String roleName, int flags, RemoteCallback callback) {
+ enforceCallerSystemUid("onClearRoleHolders");
+
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ mWorkerHandler.post(() -> RoleControllerService.this.onClearRoleHolders(roleName,
+ flags, callback));
+ }
+
+ private void enforceCallerSystemUid(@NonNull String methodName) {
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Only the system process can call " + methodName
+ + "()");
+ }
+ }
+
+ @Override
+ public void isApplicationQualifiedForRole(String roleName, String packageName,
+ RemoteCallback callback) {
+ enforceCallingPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, null);
+
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName,
+ "packageName cannot be null or empty");
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ boolean qualified = onIsApplicationQualifiedForRole(roleName, packageName);
+ callback.sendResult(qualified ? Bundle.EMPTY : null);
+ }
+
+ @Override
+ public void isApplicationVisibleForRole(String roleName, String packageName,
+ RemoteCallback callback) {
+ enforceCallingPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, null);
+
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName,
+ "packageName cannot be null or empty");
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ boolean visible = onIsApplicationVisibleForRole(roleName, packageName);
+ callback.sendResult(visible ? Bundle.EMPTY : null);
+ }
+
+ @Override
+ public void isRoleVisible(String roleName, RemoteCallback callback) {
+ enforceCallingPermission(Manifest.permission.MANAGE_ROLE_HOLDERS, null);
+
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ boolean visible = onIsRoleVisible(roleName);
+ callback.sendResult(visible ? Bundle.EMPTY : null);
+ }
+ };
+ }
+
+ private void grantDefaultRoles(@NonNull RemoteCallback callback) {
+ boolean successful = onGrantDefaultRoles();
+ callback.sendResult(successful ? Bundle.EMPTY : null);
+ }
+
+ private void onAddRoleHolder(@NonNull String roleName, @NonNull String packageName,
+ @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) {
+ boolean successful = onAddRoleHolder(roleName, packageName, flags);
+ callback.sendResult(successful ? Bundle.EMPTY : null);
+ }
+
+ private void onRemoveRoleHolder(@NonNull String roleName, @NonNull String packageName,
+ @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) {
+ boolean successful = onRemoveRoleHolder(roleName, packageName, flags);
+ callback.sendResult(successful ? Bundle.EMPTY : null);
+ }
+
+ private void onClearRoleHolders(@NonNull String roleName,
+ @RoleManager.ManageHoldersFlags int flags, @NonNull RemoteCallback callback) {
+ boolean successful = onClearRoleHolders(roleName, flags);
+ callback.sendResult(successful ? Bundle.EMPTY : null);
+ }
+
+ /**
+ * Called by system to grant default permissions and roles.
+ * <p>
+ * This is typically when creating a new user or upgrading either system or
+ * permission controller package
+ *
+ * @return whether this call was successful
+ */
+ @WorkerThread
+ public abstract boolean onGrantDefaultRoles();
+
+ /**
+ * Add a specific application to the holders of a role. If the role is exclusive, the previous
+ * holder will be replaced.
+ * <p>
+ * Implementation should enforce the role requirements and grant or revoke the relevant
+ * privileges of roles.
+ *
+ * @param roleName the name of the role to add the role holder for
+ * @param packageName the package name of the application to add to the role holders
+ * @param flags optional behavior flags
+ *
+ * @return whether this call was successful
+ *
+ * @see RoleManager#addRoleHolderAsUser(String, String, int, UserHandle, Executor,
+ * RemoteCallback)
+ */
+ @WorkerThread
+ public abstract boolean onAddRoleHolder(@NonNull String roleName, @NonNull String packageName,
+ @RoleManager.ManageHoldersFlags int flags);
+
+ /**
+ * Remove a specific application from the holders of a role.
+ *
+ * @param roleName the name of the role to remove the role holder for
+ * @param packageName the package name of the application to remove from the role holders
+ * @param flags optional behavior flags
+ *
+ * @return whether this call was successful
+ *
+ * @see RoleManager#removeRoleHolderAsUser(String, String, int, UserHandle, Executor,
+ * RemoteCallback)
+ */
+ @WorkerThread
+ public abstract boolean onRemoveRoleHolder(@NonNull String roleName,
+ @NonNull String packageName, @RoleManager.ManageHoldersFlags int flags);
+
+ /**
+ * Remove all holders of a role.
+ *
+ * @param roleName the name of the role to remove role holders for
+ * @param flags optional behavior flags
+ *
+ * @return whether this call was successful
+ *
+ * @see RoleManager#clearRoleHoldersAsUser(String, int, UserHandle, Executor, RemoteCallback)
+ */
+ @WorkerThread
+ public abstract boolean onClearRoleHolders(@NonNull String roleName,
+ @RoleManager.ManageHoldersFlags int flags);
+
+ /**
+ * Check whether an application is qualified for a role.
+ *
+ * @param roleName name of the role to check for
+ * @param packageName package name of the application to check for
+ *
+ * @return whether the application is qualified for the role
+ *
+ * @deprecated Implement {@link #onIsApplicationVisibleForRole(String, String)} instead.
+ */
+ @Deprecated
+ public abstract boolean onIsApplicationQualifiedForRole(@NonNull String roleName,
+ @NonNull String packageName);
+
+ /**
+ * Check whether an application is visible for a role.
+ *
+ * While an application can be qualified for a role, it can still stay hidden from user (thus
+ * not visible). If an application is visible for a role, we may show things related to the role
+ * for it, e.g. showing an entry pointing to the role settings in its application info page.
+ *
+ * @param roleName name of the role to check for
+ * @param packageName package name of the application to check for
+ *
+ * @return whether the application is visible for the role
+ */
+ public boolean onIsApplicationVisibleForRole(@NonNull String roleName,
+ @NonNull String packageName) {
+ return onIsApplicationQualifiedForRole(roleName, packageName);
+ }
+
+ /**
+ * Check whether a role should be visible to user.
+ *
+ * @param roleName name of the role to check for
+ *
+ * @return whether the role should be visible to user
+ */
+ public abstract boolean onIsRoleVisible(@NonNull String roleName);
+}
diff --git a/android-34/android/app/role/RoleFrameworkInitializer.java b/android-34/android/app/role/RoleFrameworkInitializer.java
new file mode 100644
index 0000000..694af12
--- /dev/null
+++ b/android-34/android/app/role/RoleFrameworkInitializer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.role;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Class holding initialization code for role in the permission module.
+ *
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class RoleFrameworkInitializer {
+ private RoleFrameworkInitializer() {}
+
+ /**
+ * Called by {@link SystemServiceRegistry}'s static initializer and registers
+ * {@link RoleManager} to {@link Context}, so that {@link Context#getSystemService} can return
+ * it.
+ *
+ * <p>If this is called from other places, it throws a {@link IllegalStateException).
+ */
+ public static void registerServiceWrappers() {
+ SystemServiceRegistry.registerContextAwareService(Context.ROLE_SERVICE, RoleManager.class,
+ (context, serviceBinder) -> new RoleManager(context,
+ IRoleManager.Stub.asInterface(serviceBinder)));
+ }
+}
diff --git a/android-34/android/app/role/RoleManager.java b/android-34/android/app/role/RoleManager.java
new file mode 100644
index 0000000..de697f8
--- /dev/null
+++ b/android-34/android/app/role/RoleManager.java
@@ -0,0 +1,955 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.role;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.UserHandleAware;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Process;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * This class provides information about and manages roles.
+ * <p>
+ * A role is a unique name within the system associated with certain privileges. The list of
+ * available roles might change with a system app update, so apps should not make assumption about
+ * the availability of roles. Instead, they should always query if the role is available using
+ * {@link #isRoleAvailable(String)} before trying to do anything with it. Some predefined role names
+ * are available as constants in this class, and a list of possibly available roles can be found in
+ * the <a href="{@docRoot}reference/androidx/core/role/package-summary.html">AndroidX Role
+ * library</a>.
+ * <p>
+ * There can be multiple applications qualifying for a role, but only a subset of them can become
+ * role holders. To qualify for a role, an application must meet certain requirements, including
+ * defining certain components in its manifest. These requirements can be found in the AndroidX
+ * Libraries. Then the application will need user consent to become a role holder, which can be
+ * requested using {@link android.app.Activity#startActivityForResult(Intent, int)} with the
+ * {@code Intent} obtained from {@link #createRequestRoleIntent(String)}.
+ * <p>
+ * Upon becoming a role holder, the application may be granted certain privileges that are role
+ * specific. When the application loses its role, these privileges will also be revoked.
+ */
+@SystemService(Context.ROLE_SERVICE)
+public final class RoleManager {
+ /**
+ * The name of the assistant app role.
+ *
+ * @see android.service.voice.VoiceInteractionService
+ */
+ public static final String ROLE_ASSISTANT = "android.app.role.ASSISTANT";
+
+ /**
+ * The name of the browser role.
+ *
+ * @see Intent#CATEGORY_APP_BROWSER
+ */
+ public static final String ROLE_BROWSER = "android.app.role.BROWSER";
+
+ /**
+ * The name of the dialer role.
+ *
+ * @see Intent#ACTION_DIAL
+ * @see android.telecom.InCallService
+ */
+ public static final String ROLE_DIALER = "android.app.role.DIALER";
+
+ /**
+ * The name of the SMS role.
+ *
+ * @see Intent#CATEGORY_APP_MESSAGING
+ */
+ public static final String ROLE_SMS = "android.app.role.SMS";
+
+ /**
+ * The name of the emergency role
+ */
+ public static final String ROLE_EMERGENCY = "android.app.role.EMERGENCY";
+
+ /**
+ * The name of the home role.
+ *
+ * @see Intent#CATEGORY_HOME
+ */
+ public static final String ROLE_HOME = "android.app.role.HOME";
+
+ /**
+ * The name of the call redirection role.
+ * <p>
+ * A call redirection app provides a means to re-write the phone number for an outgoing call to
+ * place the call through a call redirection service.
+ *
+ * @see android.telecom.CallRedirectionService
+ */
+ public static final String ROLE_CALL_REDIRECTION = "android.app.role.CALL_REDIRECTION";
+
+ /**
+ * The name of the call screening and caller id role.
+ *
+ * @see android.telecom.CallScreeningService
+ */
+ public static final String ROLE_CALL_SCREENING = "android.app.role.CALL_SCREENING";
+
+ /**
+ * The name of the notes role.
+ *
+ * @see Intent#ACTION_CREATE_NOTE
+ * @see Intent#EXTRA_USE_STYLUS_MODE
+ */
+ public static final String ROLE_NOTES = "android.app.role.NOTES";
+
+ /**
+ * The name of the system wellbeing role.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ROLE_SYSTEM_WELLBEING = "android.app.role.SYSTEM_WELLBEING";
+
+ /**
+ * The name of the system supervision role.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ROLE_SYSTEM_SUPERVISION = "android.app.role.SYSTEM_SUPERVISION";
+
+ /**
+ * The name of the system activity recognizer role.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ROLE_SYSTEM_ACTIVITY_RECOGNIZER =
+ "android.app.role.SYSTEM_ACTIVITY_RECOGNIZER";
+
+ /**
+ * The name of the device policy management role.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ROLE_DEVICE_POLICY_MANAGEMENT =
+ "android.app.role.DEVICE_POLICY_MANAGEMENT";
+
+ /**
+ * The name of the financed device kiosk role.
+ *
+ * A financed device is a device purchased through a creditor and typically paid back under an
+ * installment plan.
+ * The creditor has the ability to lock a financed device in case of payment default.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ROLE_FINANCED_DEVICE_KIOSK =
+ "android.app.role.FINANCED_DEVICE_KIOSK";
+
+ /**
+ * The name of the system call streaming role.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String ROLE_SYSTEM_CALL_STREAMING =
+ "android.app.role.SYSTEM_CALL_STREAMING";
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = true, value = { MANAGE_HOLDERS_FLAG_DONT_KILL_APP })
+ public @interface ManageHoldersFlags {}
+
+ /**
+ * Flag parameter for {@link #addRoleHolderAsUser}, {@link #removeRoleHolderAsUser} and
+ * {@link #clearRoleHoldersAsUser} to indicate that apps should not be killed when changing
+ * their role holder status.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int MANAGE_HOLDERS_FLAG_DONT_KILL_APP = 1;
+
+ /**
+ * The action used to request user approval of a role for an application.
+ *
+ * @hide
+ */
+ public static final String ACTION_REQUEST_ROLE = "android.app.role.action.REQUEST_ROLE";
+
+ /**
+ * The permission required to manage records of role holders in {@link RoleManager} directly.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_MANAGE_ROLES_FROM_CONTROLLER =
+ "com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER";
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final IRoleManager mService;
+
+ @GuardedBy("mListenersLock")
+ @NonNull
+ private final SparseArray<ArrayMap<OnRoleHoldersChangedListener,
+ OnRoleHoldersChangedListenerDelegate>> mListeners = new SparseArray<>();
+ @NonNull
+ private final Object mListenersLock = new Object();
+
+ @GuardedBy("mRoleControllerManagerLock")
+ @Nullable
+ private RoleControllerManager mRoleControllerManager;
+ private final Object mRoleControllerManagerLock = new Object();
+
+ /**
+ * Create a new instance of this class.
+ *
+ * @param context the {@link Context}
+ * @param service the {@link IRoleManager} service
+ *
+ * @hide
+ */
+ public RoleManager(@NonNull Context context, @NonNull IRoleManager service) {
+ mContext = context;
+ mService = service;
+ }
+
+ /**
+ * Returns an {@code Intent} suitable for passing to
+ * {@link android.app.Activity#startActivityForResult(Intent, int)} which prompts the user to
+ * grant a role to this application.
+ * <p>
+ * If the role is granted, the {@code resultCode} will be
+ * {@link android.app.Activity#RESULT_OK}, otherwise it will be
+ * {@link android.app.Activity#RESULT_CANCELED}.
+ *
+ * @param roleName the name of requested role
+ *
+ * @return the {@code Intent} to prompt user to grant the role
+ */
+ @NonNull
+ public Intent createRequestRoleIntent(@NonNull String roleName) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Intent intent = new Intent(ACTION_REQUEST_ROLE);
+ intent.setPackage(mContext.getPackageManager().getPermissionControllerPackageName());
+ intent.putExtra(Intent.EXTRA_ROLE_NAME, roleName);
+ return intent;
+ }
+
+ /**
+ * Check whether a role is available in the system.
+ *
+ * @param roleName the name of role to checking for
+ *
+ * @return whether the role is available in the system
+ */
+ public boolean isRoleAvailable(@NonNull String roleName) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ try {
+ return mService.isRoleAvailable(roleName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Check whether the calling application is holding a particular role.
+ *
+ * @param roleName the name of the role to check for
+ *
+ * @return whether the calling application is holding the role
+ */
+ public boolean isRoleHeld(@NonNull String roleName) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ try {
+ return mService.isRoleHeld(roleName, mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get package names of the applications holding the role.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.MANAGE_ROLE_HOLDERS}.
+ *
+ * @param roleName the name of the role to get the role holder for
+ *
+ * @return a list of package names of the role holders, or an empty list if none.
+ *
+ * @see #getRoleHoldersAsUser(String, UserHandle)
+ *
+ * @hide
+ */
+ @NonNull
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ @SystemApi
+ public List<String> getRoleHolders(@NonNull String roleName) {
+ return getRoleHoldersAsUser(roleName, Process.myUserHandle());
+ }
+
+ /**
+ * Get package names of the applications holding the role.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.MANAGE_ROLE_HOLDERS} and if the user id is not the current user
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @param roleName the name of the role to get the role holder for
+ * @param user the user to get the role holder for
+ *
+ * @return a list of package names of the role holders, or an empty list if none.
+ *
+ * @see #addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
+ * @see #removeRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
+ * @see #clearRoleHoldersAsUser(String, int, UserHandle, Executor, Consumer)
+ *
+ * @hide
+ */
+ @NonNull
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ @SystemApi
+ public List<String> getRoleHoldersAsUser(@NonNull String roleName, @NonNull UserHandle user) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Objects.requireNonNull(user, "user cannot be null");
+ try {
+ return mService.getRoleHoldersAsUser(roleName, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Add a specific application to the holders of a role. If the role is exclusive, the previous
+ * holder will be replaced.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.MANAGE_ROLE_HOLDERS} and if the user id is not the current user
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @param roleName the name of the role to add the role holder for
+ * @param packageName the package name of the application to add to the role holders
+ * @param flags optional behavior flags
+ * @param user the user to add the role holder for
+ * @param executor the {@code Executor} to run the callback on.
+ * @param callback the callback for whether this call is successful
+ *
+ * @see #getRoleHoldersAsUser(String, UserHandle)
+ * @see #removeRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
+ * @see #clearRoleHoldersAsUser(String, int, UserHandle, Executor, Consumer)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ @SystemApi
+ public void addRoleHolderAsUser(@NonNull String roleName, @NonNull String packageName,
+ @ManageHoldersFlags int flags, @NonNull UserHandle user,
+ @CallbackExecutor @NonNull Executor executor, @NonNull Consumer<Boolean> callback) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
+ Objects.requireNonNull(user, "user cannot be null");
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ try {
+ mService.addRoleHolderAsUser(roleName, packageName, flags, user.getIdentifier(),
+ createRemoteCallback(executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove a specific application from the holders of a role.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.MANAGE_ROLE_HOLDERS} and if the user id is not the current user
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @param roleName the name of the role to remove the role holder for
+ * @param packageName the package name of the application to remove from the role holders
+ * @param flags optional behavior flags
+ * @param user the user to remove the role holder for
+ * @param executor the {@code Executor} to run the callback on.
+ * @param callback the callback for whether this call is successful
+ *
+ * @see #getRoleHoldersAsUser(String, UserHandle)
+ * @see #addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
+ * @see #clearRoleHoldersAsUser(String, int, UserHandle, Executor, Consumer)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ @SystemApi
+ public void removeRoleHolderAsUser(@NonNull String roleName, @NonNull String packageName,
+ @ManageHoldersFlags int flags, @NonNull UserHandle user,
+ @CallbackExecutor @NonNull Executor executor, @NonNull Consumer<Boolean> callback) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
+ Objects.requireNonNull(user, "user cannot be null");
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ try {
+ mService.removeRoleHolderAsUser(roleName, packageName, flags, user.getIdentifier(),
+ createRemoteCallback(executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove all holders of a role.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.MANAGE_ROLE_HOLDERS} and if the user id is not the current user
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @param roleName the name of the role to remove role holders for
+ * @param flags optional behavior flags
+ * @param user the user to remove role holders for
+ * @param executor the {@code Executor} to run the callback on.
+ * @param callback the callback for whether this call is successful
+ *
+ * @see #getRoleHoldersAsUser(String, UserHandle)
+ * @see #addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
+ * @see #removeRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ @SystemApi
+ public void clearRoleHoldersAsUser(@NonNull String roleName, @ManageHoldersFlags int flags,
+ @NonNull UserHandle user, @CallbackExecutor @NonNull Executor executor,
+ @NonNull Consumer<Boolean> callback) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Objects.requireNonNull(user, "user cannot be null");
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ try {
+ mService.clearRoleHoldersAsUser(roleName, flags, user.getIdentifier(),
+ createRemoteCallback(executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get package names of the applications holding the role for a default application.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.MANAGE_DEFAULT_APPLICATIONS}.
+ *
+ * @param roleName the name of the default application role to get
+ *
+ * @return a package name of the role holder or {@code null} if not set.
+ *
+ * @see #setDefaultApplication(String, String, int, Executor, Consumer)
+ *
+ * @hide
+ */
+ @Nullable
+ @RequiresPermission(Manifest.permission.MANAGE_DEFAULT_APPLICATIONS)
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @UserHandleAware
+ @SystemApi
+ public String getDefaultApplication(@NonNull String roleName) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ try {
+ return mService.getDefaultApplicationAsUser(
+ roleName, mContext.getUser().getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set a specific application as the default application.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.MANAGE_DEFAULT_APPLICATIONS}.
+ *
+ * @param roleName the name of the default application role to set the role holder for
+ * @param packageName the package name of the application to set as the default application,
+ * or {@code null} to unset.
+ * @param flags optional behavior flags
+ * @param executor the {@code Executor} to run the callback on.
+ * @param callback the callback for whether this call is successful
+ *
+ * @see #getDefaultApplication(String)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_DEFAULT_APPLICATIONS)
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @UserHandleAware
+ @SystemApi
+ public void setDefaultApplication(@NonNull String roleName, @Nullable String packageName,
+ @ManageHoldersFlags int flags, @CallbackExecutor @NonNull Executor executor,
+ @NonNull Consumer<Boolean> callback) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ try {
+ mService.setDefaultApplicationAsUser(roleName, packageName, flags,
+ mContext.getUser().getIdentifier(), createRemoteCallback(executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @NonNull
+ private static RemoteCallback createRemoteCallback(@NonNull Executor executor,
+ @NonNull Consumer<Boolean> callback) {
+ return new RemoteCallback(result -> executor.execute(() -> {
+ boolean successful = result != null;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ callback.accept(successful);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }));
+ }
+
+ /**
+ * Add a listener to observe role holder changes
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.OBSERVE_ROLE_HOLDERS} and if the user id is not the current user
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @param executor the {@code Executor} to call the listener on.
+ * @param listener the listener to be added
+ * @param user the user to add the listener for
+ *
+ * @see #removeOnRoleHoldersChangedListenerAsUser(OnRoleHoldersChangedListener, UserHandle)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS)
+ @SuppressLint("SamShouldBeLast") // TODO(b/190240500): remove this
+ @SystemApi
+ public void addOnRoleHoldersChangedListenerAsUser(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OnRoleHoldersChangedListener listener, @NonNull UserHandle user) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(listener, "listener cannot be null");
+ Objects.requireNonNull(user, "user cannot be null");
+ int userId = user.getIdentifier();
+ synchronized (mListenersLock) {
+ ArrayMap<OnRoleHoldersChangedListener, OnRoleHoldersChangedListenerDelegate> listeners =
+ mListeners.get(userId);
+ if (listeners == null) {
+ listeners = new ArrayMap<>();
+ mListeners.put(userId, listeners);
+ } else {
+ if (listeners.containsKey(listener)) {
+ return;
+ }
+ }
+ OnRoleHoldersChangedListenerDelegate listenerDelegate =
+ new OnRoleHoldersChangedListenerDelegate(executor, listener);
+ try {
+ mService.addOnRoleHoldersChangedListenerAsUser(listenerDelegate, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ listeners.put(listener, listenerDelegate);
+ }
+ }
+
+ /**
+ * Remove a listener observing role holder changes
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@code android.permission.OBSERVE_ROLE_HOLDERS} and if the user id is not the current user
+ * {@code android.permission.INTERACT_ACROSS_USERS_FULL}.
+ *
+ * @param listener the listener to be removed
+ * @param user the user to remove the listener for
+ *
+ * @see #addOnRoleHoldersChangedListenerAsUser(Executor, OnRoleHoldersChangedListener,
+ * UserHandle)
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS)
+ @SuppressLint("SamShouldBeLast") // TODO(b/190240500): remove this
+ @SystemApi
+ public void removeOnRoleHoldersChangedListenerAsUser(
+ @NonNull OnRoleHoldersChangedListener listener, @NonNull UserHandle user) {
+ Objects.requireNonNull(listener, "listener cannot be null");
+ Objects.requireNonNull(user, "user cannot be null");
+ int userId = user.getIdentifier();
+ synchronized (mListenersLock) {
+ ArrayMap<OnRoleHoldersChangedListener, OnRoleHoldersChangedListenerDelegate> listeners =
+ mListeners.get(userId);
+ if (listeners == null) {
+ return;
+ }
+ OnRoleHoldersChangedListenerDelegate listenerDelegate = listeners.get(listener);
+ if (listenerDelegate == null) {
+ return;
+ }
+ try {
+ mService.removeOnRoleHoldersChangedListenerAsUser(listenerDelegate,
+ user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ listeners.remove(listener);
+ if (listeners.isEmpty()) {
+ mListeners.remove(userId);
+ }
+ }
+ }
+
+ /**
+ * Check whether role qualifications should be bypassed.
+ * <p>
+ * Only the shell is allowed to do this, the qualification for the shell role itself cannot be
+ * bypassed, and each role needs to explicitly allow bypassing qualification in its definition.
+ * The bypass state will not be persisted across reboot.
+ *
+ * @return whether role qualification should be bypassed
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ @SystemApi
+ public boolean isBypassingRoleQualification() {
+ try {
+ return mService.isBypassingRoleQualification();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set whether role qualifications should be bypassed.
+ * <p>
+ * Only the shell is allowed to do this, the qualification for the shell role itself cannot be
+ * bypassed, and each role needs to explicitly allow bypassing qualification in its definition.
+ * The bypass state will not be persisted across reboot.
+ *
+ * @param bypassRoleQualification whether role qualification should be bypassed
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresPermission(Manifest.permission.BYPASS_ROLE_QUALIFICATION)
+ @SystemApi
+ public void setBypassingRoleQualification(boolean bypassRoleQualification) {
+ try {
+ mService.setBypassingRoleQualification(bypassRoleQualification);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set the names of all the available roles. Should only be called from
+ * {@link android.app.role.RoleControllerService}.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@link #PERMISSION_MANAGE_ROLES_FROM_CONTROLLER}.
+ *
+ * @param roleNames the names of all the available roles
+ *
+ * @deprecated This is only usable by the role controller service, which is an internal
+ * implementation detail inside role.
+ *
+ * @hide
+ */
+ @Deprecated
+ @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
+ @SystemApi
+ public void setRoleNamesFromController(@NonNull List<String> roleNames) {
+ Objects.requireNonNull(roleNames, "roleNames cannot be null");
+ try {
+ mService.setRoleNamesFromController(roleNames);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Add a specific application to the holders of a role, only modifying records inside
+ * {@link RoleManager}. Should only be called from
+ * {@link android.app.role.RoleControllerService}.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@link #PERMISSION_MANAGE_ROLES_FROM_CONTROLLER}.
+ *
+ * @param roleName the name of the role to add the role holder for
+ * @param packageName the package name of the application to add to the role holders
+ *
+ * @return whether the operation was successful, and will also be {@code true} if a matching
+ * role holder is already found.
+ *
+ * @see #getRoleHolders(String)
+ * @see #removeRoleHolderFromController(String, String)
+ *
+ * @deprecated This is only usable by the role controller service, which is an internal
+ * implementation detail inside role.
+ *
+ * @hide
+ */
+ @Deprecated
+ @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
+ @SystemApi
+ public boolean addRoleHolderFromController(@NonNull String roleName,
+ @NonNull String packageName) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
+ try {
+ return mService.addRoleHolderFromController(roleName, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove a specific application from the holders of a role, only modifying records inside
+ * {@link RoleManager}. Should only be called from
+ * {@link android.app.role.RoleControllerService}.
+ * <p>
+ * <strong>Note:</strong> Using this API requires holding
+ * {@link #PERMISSION_MANAGE_ROLES_FROM_CONTROLLER}.
+ *
+ * @param roleName the name of the role to remove the role holder for
+ * @param packageName the package name of the application to remove from the role holders
+ *
+ * @return whether the operation was successful, and will also be {@code true} if no matching
+ * role holder was found to remove.
+ *
+ * @see #getRoleHolders(String)
+ * @see #addRoleHolderFromController(String, String)
+ *
+ * @deprecated This is only usable by the role controller service, which is an internal
+ * implementation detail inside role.
+ *
+ * @hide
+ */
+ @Deprecated
+ @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
+ @SystemApi
+ public boolean removeRoleHolderFromController(@NonNull String roleName,
+ @NonNull String packageName) {
+ Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
+ Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
+ try {
+ return mService.removeRoleHolderFromController(roleName, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the list of all roles that the given package is currently holding
+ *
+ * @param packageName the package name
+ * @return the list of role names
+ *
+ * @deprecated This is only usable by the role controller service, which is an internal
+ * implementation detail inside role.
+ *
+ * @hide
+ */
+ @Deprecated
+ @NonNull
+ @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER)
+ @SystemApi
+ public List<String> getHeldRolesFromController(@NonNull String packageName) {
+ Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty");
+ try {
+ return mService.getHeldRolesFromController(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the role holder of {@link #ROLE_BROWSER} without requiring
+ * {@link Manifest.permission#OBSERVE_ROLE_HOLDERS}, as in
+ * {@link android.content.pm.PackageManager#getDefaultBrowserPackageNameAsUser(int)}
+ *
+ * @param userId the user ID
+ * @return the package name of the default browser, or {@code null} if none
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @Nullable
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public String getBrowserRoleHolder(@UserIdInt int userId) {
+ try {
+ return mService.getBrowserRoleHolder(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set the role holder of {@link #ROLE_BROWSER} requiring
+ * {@link Manifest.permission.SET_PREFERRED_APPLICATIONS} instead of
+ * {@link Manifest.permission#MANAGE_ROLE_HOLDERS}, as in
+ * {@link android.content.pm.PackageManager#setDefaultBrowserPackageNameAsUser(String, int)}
+ *
+ * @param packageName the package name of the default browser, or {@code null} if none
+ * @param userId the user ID
+ * @return whether the default browser was set successfully
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @Nullable
+ @RequiresPermission(Manifest.permission.SET_PREFERRED_APPLICATIONS)
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public boolean setBrowserRoleHolder(@Nullable String packageName, @UserIdInt int userId) {
+ try {
+ return mService.setBrowserRoleHolder(packageName, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allows getting the role holder for {@link #ROLE_SMS} without requiring
+ * {@link Manifest.permission#OBSERVE_ROLE_HOLDERS}, as in
+ * {@link android.provider.Telephony.Sms#getDefaultSmsPackage(Context)}.
+ *
+ * @param userId the user ID to get the default SMS package for
+ * @return the package name of the default SMS app, or {@code null} if none
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @Nullable
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public String getSmsRoleHolder(@UserIdInt int userId) {
+ try {
+ return mService.getSmsRoleHolder(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Check whether a role should be visible to user.
+ *
+ * @param roleName name of the role to check for
+ * @param executor the executor to execute callback on
+ * @param callback the callback to receive whether the role should be visible to user
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ @SystemApi
+ public void isRoleVisible(@NonNull String roleName,
+ @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) {
+ getRoleControllerManager().isRoleVisible(roleName, executor, callback);
+ }
+
+ /**
+ * Check whether an application is visible for a role.
+ *
+ * While an application can be qualified for a role, it can still stay hidden from user (thus
+ * not visible). If an application is visible for a role, we may show things related to the role
+ * for it, e.g. showing an entry pointing to the role settings in its application info page.
+ *
+ * @param roleName the name of the role to check for
+ * @param packageName the package name of the application to check for
+ * @param executor the executor to execute callback on
+ * @param callback the callback to receive whether the application is visible for the role
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @RequiresPermission(Manifest.permission.MANAGE_ROLE_HOLDERS)
+ @SystemApi
+ public void isApplicationVisibleForRole(@NonNull String roleName, @NonNull String packageName,
+ @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) {
+ getRoleControllerManager().isApplicationVisibleForRole(roleName, packageName, executor,
+ callback);
+ }
+
+ @NonNull
+ private RoleControllerManager getRoleControllerManager() {
+ synchronized (mRoleControllerManagerLock) {
+ if (mRoleControllerManager == null) {
+ mRoleControllerManager = new RoleControllerManager(mContext);
+ }
+ return mRoleControllerManager;
+ }
+ }
+
+ private static class OnRoleHoldersChangedListenerDelegate
+ extends IOnRoleHoldersChangedListener.Stub {
+
+ @NonNull
+ private final Executor mExecutor;
+ @NonNull
+ private final OnRoleHoldersChangedListener mListener;
+
+ OnRoleHoldersChangedListenerDelegate(@NonNull Executor executor,
+ @NonNull OnRoleHoldersChangedListener listener) {
+ mExecutor = executor;
+ mListener = listener;
+ }
+
+ @Override
+ public void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() ->
+ mListener.onRoleHoldersChanged(roleName, UserHandle.of(userId)));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/sdksandbox/FileUtil.java b/android-34/android/app/sdksandbox/FileUtil.java
new file mode 100644
index 0000000..4075a63
--- /dev/null
+++ b/android-34/android/app/sdksandbox/FileUtil.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import java.io.File;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Utility class for performing file related operations
+ *
+ * @hide
+ */
+public class FileUtil {
+
+ public static final String TAG = "SdkSandboxManager";
+ public static final int CONVERSION_FACTOR_FROM_BYTES_TO_KB = 1024;
+
+ /** Calculate the storage of SDK iteratively */
+ public static int getStorageInKbForPaths(List<String> paths) {
+ float storageSize = 0;
+ for (int i = 0; i < paths.size(); i++) {
+ final File dir = new File(paths.get(i));
+
+ if (Objects.nonNull(dir)) {
+ storageSize += getStorageForFiles(dir.listFiles());
+ }
+ }
+ return convertByteToKb(storageSize);
+ }
+
+ private static float getStorageForFiles(File[] files) {
+ if (Objects.isNull(files)) {
+ return 0;
+ }
+
+ float sizeInBytes = 0;
+
+ for (File file : files) {
+ if (file.isDirectory()) {
+ sizeInBytes += getStorageForFiles(file.listFiles());
+ } else {
+ sizeInBytes += file.length();
+ }
+ }
+ return sizeInBytes;
+ }
+
+ private static int convertByteToKb(float storageSize) {
+ return (int) (storageSize / CONVERSION_FACTOR_FROM_BYTES_TO_KB);
+ }
+}
diff --git a/android-34/android/app/sdksandbox/LoadSdkException.java b/android-34/android/app/sdksandbox/LoadSdkException.java
new file mode 100644
index 0000000..9a77201
--- /dev/null
+++ b/android-34/android/app/sdksandbox/LoadSdkException.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/** Exception thrown by {@link SdkSandboxManager#loadSdk} */
+public final class LoadSdkException extends Exception implements Parcelable {
+
+ @SdkSandboxManager.LoadSdkErrorCode private final int mLoadSdkErrorCode;
+ private final Bundle mExtraInformation;
+
+ /**
+ * Initializes a {@link LoadSdkException} with a Throwable and a Bundle.
+ *
+ * @param cause The cause of the exception, which is saved for later retrieval by the {@link
+ * #getCause()} method.
+ * @param extraInfo Extra error information. This is empty if there is no such information.
+ */
+ public LoadSdkException(@NonNull Throwable cause, @NonNull Bundle extraInfo) {
+ this(SdkSandboxManager.LOAD_SDK_SDK_DEFINED_ERROR, cause.getMessage(), cause, extraInfo);
+ }
+
+ /**
+ * Initializes a {@link LoadSdkException} with a result code and a message
+ *
+ * @param loadSdkErrorCode The result code.
+ * @param message The detailed message which is saved for later retrieval by the {@link
+ * #getMessage()} method.
+ * @hide
+ */
+ public LoadSdkException(
+ @SdkSandboxManager.LoadSdkErrorCode int loadSdkErrorCode, @Nullable String message) {
+ this(loadSdkErrorCode, message, /*cause=*/ null);
+ }
+
+ /**
+ * Initializes a {@link LoadSdkException} with a result code, a message and a cause.
+ *
+ * @param loadSdkErrorCode The result code.
+ * @param message The detailed message which is saved for later retrieval by the {@link
+ * #getMessage()} method.
+ * @param cause The cause of the exception, which is saved for later retrieval by the {@link
+ * #getCause()} method. A null value is permitted, and indicates that the cause is
+ * nonexistent or unknown.
+ * @hide
+ */
+ public LoadSdkException(
+ @SdkSandboxManager.LoadSdkErrorCode int loadSdkErrorCode,
+ @Nullable String message,
+ @Nullable Throwable cause) {
+ this(loadSdkErrorCode, message, cause, new Bundle());
+ }
+
+ /**
+ * Initializes a {@link LoadSdkException} with a result code, a message, a cause and extra
+ * information.
+ *
+ * @param loadSdkErrorCode The result code.
+ * @param message The detailed message which is saved for later retrieval by the {@link
+ * #getMessage()} method.
+ * @param cause The cause of the exception, which is saved for later retrieval by the {@link
+ * #getCause()} method. A null value is permitted, and indicates that the cause is
+ * nonexistent or unknown.
+ * @param extraInfo Extra error information. This is empty if there is no such information.
+ * @hide
+ */
+ public LoadSdkException(
+ @SdkSandboxManager.LoadSdkErrorCode int loadSdkErrorCode,
+ @Nullable String message,
+ @Nullable Throwable cause,
+ @NonNull Bundle extraInfo) {
+ super(message, cause);
+ mLoadSdkErrorCode = loadSdkErrorCode;
+ mExtraInformation = extraInfo;
+ }
+
+ @NonNull
+ public static final Creator<LoadSdkException> CREATOR =
+ new Creator<LoadSdkException>() {
+ @Override
+ public LoadSdkException createFromParcel(Parcel in) {
+ int errorCode = in.readInt();
+ String message = in.readString();
+ Bundle extraInformation = in.readBundle();
+ return new LoadSdkException(errorCode, message, null, extraInformation);
+ }
+
+ @Override
+ public LoadSdkException[] newArray(int size) {
+ return new LoadSdkException[size];
+ }
+ };
+
+ /**
+ * Returns the result code this exception was constructed with.
+ *
+ * @return The loadSdk result code.
+ */
+ @SdkSandboxManager.LoadSdkErrorCode
+ public int getLoadSdkErrorCode() {
+ return mLoadSdkErrorCode;
+ }
+
+ /**
+ * Returns the extra error information this exception was constructed with.
+ *
+ * @return The extra error information Bundle.
+ */
+ @NonNull
+ public Bundle getExtraInformation() {
+ return mExtraInformation;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel destination, int flags) {
+ destination.writeInt(mLoadSdkErrorCode);
+ destination.writeString(this.getMessage());
+ destination.writeBundle(mExtraInformation);
+ }
+}
diff --git a/android-34/android/app/sdksandbox/LogUtil.java b/android-34/android/app/sdksandbox/LogUtil.java
new file mode 100644
index 0000000..2e414bc
--- /dev/null
+++ b/android-34/android/app/sdksandbox/LogUtil.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.util.Log;
+
+/**
+ * Utility class for logging behind {@link Log#isLoggable} check.
+ *
+ * <p>Logging can be enabled by running {@code adb shell setprop persist.log.tag.SDK_SANDBOX DEBUG}
+ * or individual tag used while logging with {@link LogUtil}.
+ *
+ * @hide
+ */
+public class LogUtil {
+
+ public static final String GUARD_TAG = "SDK_SANDBOX";
+
+ /** Log the message as VERBOSE. Return The number of bytes written. */
+ public static int v(String tag, String msg) {
+ if (Log.isLoggable(GUARD_TAG, Log.VERBOSE) || Log.isLoggable(tag, Log.VERBOSE)) {
+ return Log.v(tag, msg);
+ }
+ return 0;
+ }
+
+ /** Log the message as DEBUG. Return The number of bytes written. */
+ public static int d(String tag, String msg) {
+ if (Log.isLoggable(GUARD_TAG, Log.DEBUG) || Log.isLoggable(tag, Log.DEBUG)) {
+ return Log.d(tag, msg);
+ }
+ return 0;
+ }
+}
diff --git a/android-34/android/app/sdksandbox/RequestSurfacePackageException.java b/android-34/android/app/sdksandbox/RequestSurfacePackageException.java
new file mode 100644
index 0000000..44fbeef
--- /dev/null
+++ b/android-34/android/app/sdksandbox/RequestSurfacePackageException.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+/** Exception thrown by {@link SdkSandboxManager#requestSurfacePackage} */
+public final class RequestSurfacePackageException extends Exception {
+
+ private final @SdkSandboxManager.RequestSurfacePackageErrorCode int
+ mRequestSurfacePackageErrorCode;
+ private final Bundle mExtraInformation;
+
+ /**
+ * Initializes a {@link RequestSurfacePackageException} with a result code and a message
+ *
+ * @param requestSurfacePackageErrorCode The result code.
+ * @param message The detailed message which is saved for later retrieval by the {@link
+ * #getMessage()} method.
+ */
+ public RequestSurfacePackageException(
+ @SdkSandboxManager.RequestSurfacePackageErrorCode int requestSurfacePackageErrorCode,
+ @Nullable String message) {
+ this(requestSurfacePackageErrorCode, message, /*cause=*/ null);
+ }
+
+ /**
+ * Initializes a {@link RequestSurfacePackageException} with a result code, a message and a
+ * cause.
+ *
+ * @param requestSurfacePackageErrorCode The result code.
+ * @param message The detailed message which is saved for later retrieval by the {@link
+ * #getMessage()} method.
+ * @param cause The cause of the exception, which is saved for later retrieval by the {@link
+ * #getCause()} method. A null value is permitted, and indicates that the cause is
+ * nonexistent or unknown.
+ */
+ public RequestSurfacePackageException(
+ @SdkSandboxManager.RequestSurfacePackageErrorCode int requestSurfacePackageErrorCode,
+ @Nullable String message,
+ @Nullable Throwable cause) {
+ this(requestSurfacePackageErrorCode, message, cause, new Bundle());
+ }
+
+ /**
+ * Initializes a {@link RequestSurfacePackageException} with a result code, a message, a cause
+ * and extra information.
+ *
+ * @param requestSurfacePackageErrorCode The result code.
+ * @param message The detailed message which is saved for later retrieval by the {@link
+ * #getMessage()} method.
+ * @param cause The cause of the exception, which is saved for later retrieval by the {@link
+ * #getCause()} method. A null value is permitted, and indicates that the cause is
+ * nonexistent or unknown.
+ * @param extraInfo Extra error information. This is empty if there is no such information.
+ */
+ public RequestSurfacePackageException(
+ @SdkSandboxManager.RequestSurfacePackageErrorCode int requestSurfacePackageErrorCode,
+ @Nullable String message,
+ @Nullable Throwable cause,
+ @NonNull Bundle extraInfo) {
+ super(message, cause);
+ mRequestSurfacePackageErrorCode = requestSurfacePackageErrorCode;
+ mExtraInformation = extraInfo;
+ }
+ /**
+ * Returns the result code this exception was constructed with.
+ *
+ * @return The result code from {@link SdkSandboxManager#requestSurfacePackage}
+ */
+ public @SdkSandboxManager.RequestSurfacePackageErrorCode int
+ getRequestSurfacePackageErrorCode() {
+ return mRequestSurfacePackageErrorCode;
+ }
+
+ /**
+ * Returns the extra error information this exception was constructed with.
+ *
+ * @return The extra error information Bundle.
+ */
+ @NonNull
+ public Bundle getExtraErrorInformation() {
+ return mExtraInformation;
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SandboxedSdk.java b/android-34/android/app/sdksandbox/SandboxedSdk.java
new file mode 100644
index 0000000..8e6482c
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SandboxedSdk.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.SharedLibraryInfo;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represents an SDK loaded in the sandbox process.
+ *
+ * <p>Returned in response to {@link SdkSandboxManager#loadSdk}, on success. An application can
+ * obtain it by calling {@link SdkSandboxManager#loadSdk}. It should use this object to obtain an
+ * interface to the SDK through {@link #getInterface()}.
+ *
+ * <p>The SDK should create it when {@link SandboxedSdkProvider#onLoadSdk} is called, and drop all
+ * references to it when {@link SandboxedSdkProvider#beforeUnloadSdk()} is called. Additionally, the
+ * SDK should fail calls made to the {@code IBinder} returned from {@link #getInterface()} after
+ * {@link SandboxedSdkProvider#beforeUnloadSdk()} has been called.
+ */
+public final class SandboxedSdk implements Parcelable {
+ public static final @NonNull Creator<SandboxedSdk> CREATOR =
+ new Creator<SandboxedSdk>() {
+ @Override
+ public SandboxedSdk createFromParcel(Parcel in) {
+ return new SandboxedSdk(in);
+ }
+
+ @Override
+ public SandboxedSdk[] newArray(int size) {
+ return new SandboxedSdk[size];
+ }
+ };
+ private IBinder mInterface;
+ private @Nullable SharedLibraryInfo mSharedLibraryInfo;
+
+ /**
+ * Creates a {@link SandboxedSdk} object.
+ *
+ * @param sdkInterface The SDK's interface. This will be the entrypoint into the sandboxed SDK
+ * for the application. The SDK should keep this valid until it's loaded in the sandbox, and
+ * start failing calls to this interface once it has been unloaded.
+ * <p>This interface can later be retrieved using {@link #getInterface()}.
+ */
+ public SandboxedSdk(@NonNull IBinder sdkInterface) {
+ mInterface = sdkInterface;
+ }
+
+ private SandboxedSdk(@NonNull Parcel in) {
+ mInterface = in.readStrongBinder();
+ if (in.readInt() != 0) {
+ mSharedLibraryInfo = SharedLibraryInfo.CREATOR.createFromParcel(in);
+ }
+ }
+
+ /**
+ * Attaches information about the SDK like name, version and others which may be useful to
+ * identify the SDK.
+ *
+ * <p>This is used by the system service to attach the library info to the {@link SandboxedSdk}
+ * object return by the SDK after it has been loaded
+ *
+ * @param sharedLibraryInfo The SDK's library info. This contains the name, version and other
+ * details about the sdk.
+ * @throws IllegalStateException if a base sharedLibraryInfo has already been set.
+ * @hide
+ */
+ public void attachSharedLibraryInfo(@NonNull SharedLibraryInfo sharedLibraryInfo) {
+ if (mSharedLibraryInfo != null) {
+ throw new IllegalStateException("SharedLibraryInfo already set");
+ }
+ Objects.requireNonNull(sharedLibraryInfo, "SharedLibraryInfo cannot be null");
+ mSharedLibraryInfo = sharedLibraryInfo;
+ }
+
+ /**
+ * Returns the interface to the SDK that was loaded in response to {@link
+ * SdkSandboxManager#loadSdk}. A {@code null} interface is returned if the Binder has since
+ * become unavailable, in response to the SDK being unloaded.
+ */
+ public @Nullable IBinder getInterface() {
+ // This will be null if the remote SDK has been unloaded and the IBinder originally provided
+ // is now a dead object.
+ return mInterface;
+ }
+
+ /**
+ * Returns the {@link SharedLibraryInfo} for the SDK.
+ *
+ * @throws IllegalStateException if the system service has not yet attached {@link
+ * SharedLibraryInfo} to the {@link SandboxedSdk} object sent by the SDK.
+ */
+ public @NonNull SharedLibraryInfo getSharedLibraryInfo() {
+ if (mSharedLibraryInfo == null) {
+ throw new IllegalStateException(
+ "SharedLibraryInfo has not been set. This is populated by our system service "
+ + "once the SandboxedSdk is sent back from as a response to "
+ + "android.app.sdksandbox.SandboxedSdkProvider$onLoadSdk. Please use "
+ + "android.app.sdksandbox.SdkSandboxManager#getSandboxedSdks or "
+ + "android.app.sdksandbox.SdkSandboxController#getSandboxedSdks to "
+ + "get the correctly populated SandboxedSdks.");
+ }
+ return mSharedLibraryInfo;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeStrongBinder(mInterface);
+ if (mSharedLibraryInfo != null) {
+ dest.writeInt(1);
+ mSharedLibraryInfo.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SandboxedSdkContext.java b/android-34/android/app/sdksandbox/SandboxedSdkContext.java
new file mode 100644
index 0000000..c0111a7
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SandboxedSdkContext.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import static android.app.sdksandbox.SdkSandboxSystemServiceRegistry.ServiceMutator;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.ApplicationInfo;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+
+/**
+ * Refers to the context of the SDK loaded in the SDK sandbox process.
+ *
+ * <p>It is a wrapper of the client application (which loading SDK to the sandbox) context, to
+ * represent the context of the SDK loaded by that application.
+ *
+ * <p>An instance of the {@link SandboxedSdkContext} will be created by the SDK sandbox, and then
+ * attached to the {@link SandboxedSdkProvider} after the SDK is loaded.
+ *
+ * <p>Each sdk will get their own private storage directories and the file storage API on this
+ * object will utilize those areas.
+ *
+ * @hide
+ */
+public final class SandboxedSdkContext extends ContextWrapper {
+
+ private final Resources mResources;
+ private final AssetManager mAssets;
+ private final String mClientPackageName;
+ private final String mSdkName;
+ private final ApplicationInfo mSdkProviderInfo;
+ @Nullable private final File mCeDataDir;
+ @Nullable private final File mDeDataDir;
+ private final SdkSandboxSystemServiceRegistry mSdkSandboxSystemServiceRegistry;
+ private final ClassLoader mClassLoader;
+ private final boolean mCustomizedSdkContextEnabled;
+
+ public SandboxedSdkContext(
+ @NonNull Context baseContext,
+ @NonNull ClassLoader classLoader,
+ @NonNull String clientPackageName,
+ @NonNull ApplicationInfo info,
+ @NonNull String sdkName,
+ @Nullable String sdkCeDataDir,
+ @Nullable String sdkDeDataDir,
+ boolean isCustomizedSdkContextEnabled) {
+ this(
+ baseContext,
+ classLoader,
+ clientPackageName,
+ info,
+ sdkName,
+ sdkCeDataDir,
+ sdkDeDataDir,
+ isCustomizedSdkContextEnabled,
+ SdkSandboxSystemServiceRegistry.getInstance());
+ }
+
+ @VisibleForTesting
+ public SandboxedSdkContext(
+ @NonNull Context baseContext,
+ @NonNull ClassLoader classLoader,
+ @NonNull String clientPackageName,
+ @NonNull ApplicationInfo info,
+ @NonNull String sdkName,
+ @Nullable String sdkCeDataDir,
+ @Nullable String sdkDeDataDir,
+ boolean isCustomizedSdkContextEnabled,
+ SdkSandboxSystemServiceRegistry sdkSandboxSystemServiceRegistry) {
+ super(baseContext);
+ mClientPackageName = clientPackageName;
+ mSdkName = sdkName;
+ mSdkProviderInfo = info;
+ Resources resources = null;
+ try {
+ resources = baseContext.getPackageManager().getResourcesForApplication(info);
+ } catch (Exception ignored) {
+ }
+
+ if (resources != null) {
+ mResources = resources;
+ mAssets = resources.getAssets();
+ } else {
+ mResources = null;
+ mAssets = null;
+ }
+
+ mCeDataDir = (sdkCeDataDir != null) ? new File(sdkCeDataDir) : null;
+ mDeDataDir = (sdkDeDataDir != null) ? new File(sdkDeDataDir) : null;
+
+ mSdkSandboxSystemServiceRegistry = sdkSandboxSystemServiceRegistry;
+ mClassLoader = classLoader;
+ mCustomizedSdkContextEnabled = isCustomizedSdkContextEnabled;
+ }
+
+ /**
+ * Return a new Context object for the current SandboxedSdkContext but whose storage APIs are
+ * backed by sdk specific credential-protected storage.
+ *
+ * @see Context#isCredentialProtectedStorage()
+ */
+ @Override
+ @NonNull
+ public Context createCredentialProtectedStorageContext() {
+ Context newBaseContext = getBaseContext().createCredentialProtectedStorageContext();
+ return new SandboxedSdkContext(
+ newBaseContext,
+ mClassLoader,
+ mClientPackageName,
+ mSdkProviderInfo,
+ mSdkName,
+ (mCeDataDir != null) ? mCeDataDir.toString() : null,
+ (mDeDataDir != null) ? mDeDataDir.toString() : null,
+ mCustomizedSdkContextEnabled);
+ }
+
+ /**
+ * Return a new Context object for the current SandboxedSdkContext but whose storage
+ * APIs are backed by sdk specific device-protected storage.
+ *
+ * @see Context#isDeviceProtectedStorage()
+ */
+ @Override
+ @NonNull
+ public Context createDeviceProtectedStorageContext() {
+ Context newBaseContext = getBaseContext().createDeviceProtectedStorageContext();
+ return new SandboxedSdkContext(
+ newBaseContext,
+ mClassLoader,
+ mClientPackageName,
+ mSdkProviderInfo,
+ mSdkName,
+ (mCeDataDir != null) ? mCeDataDir.toString() : null,
+ (mDeDataDir != null) ? mDeDataDir.toString() : null,
+ mCustomizedSdkContextEnabled);
+ }
+
+ /**
+ * Returns the SDK name defined in the SDK's manifest.
+ */
+ @NonNull
+ public String getSdkName() {
+ return mSdkName;
+ }
+
+ /**
+ * Returns the SDK package name defined in the SDK's manifest.
+ *
+ * @hide
+ */
+ @NonNull
+ public String getSdkPackageName() {
+ return mSdkProviderInfo.packageName;
+ }
+
+ /**
+ * Returns the package name of the client application corresponding to the sandbox.
+ *
+ */
+ @NonNull
+ public String getClientPackageName() {
+ return mClientPackageName;
+ }
+
+ /** Returns the resources defined in the SDK's .apk file. */
+ @Override
+ @Nullable
+ public Resources getResources() {
+ if (mCustomizedSdkContextEnabled) {
+ return getBaseContext().getResources();
+ }
+ return mResources;
+ }
+
+ /** Returns the assets defined in the SDK's .apk file. */
+ @Override
+ @Nullable
+ public AssetManager getAssets() {
+ if (mCustomizedSdkContextEnabled) {
+ return getBaseContext().getAssets();
+ }
+ return mAssets;
+ }
+
+ /** Returns sdk-specific internal storage directory. */
+ @Override
+ @Nullable
+ public File getDataDir() {
+ if (mCustomizedSdkContextEnabled) {
+ return getBaseContext().getDataDir();
+ }
+
+ File res = null;
+ if (isCredentialProtectedStorage()) {
+ res = mCeDataDir;
+ } else if (isDeviceProtectedStorage()) {
+ res = mDeDataDir;
+ }
+ if (res == null) {
+ throw new RuntimeException("No data directory found for sdk: " + getSdkName());
+ }
+ return res;
+ }
+
+ @Override
+ @Nullable
+ public Object getSystemService(String name) {
+ if (name == null) {
+ return null;
+ }
+ Object service = getBaseContext().getSystemService(name);
+ ServiceMutator serviceMutator = mSdkSandboxSystemServiceRegistry.getServiceMutator(name);
+ if (serviceMutator != null) {
+ service = serviceMutator.setContext(service, this);
+ }
+ return service;
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ if (mCustomizedSdkContextEnabled) {
+ return getBaseContext().getClassLoader();
+ }
+ return mClassLoader;
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SandboxedSdkProvider.java b/android-34/android/app/sdksandbox/SandboxedSdkProvider.java
new file mode 100644
index 0000000..7108ccd
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SandboxedSdkProvider.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.sdksandbox.sdkprovider.SdkSandboxController;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+import android.view.View;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates API which SDK sandbox can use to interact with SDKs loaded into it.
+ *
+ * <p>SDK has to implement this abstract class to generate an entry point for SDK sandbox to be able
+ * to call it through.
+ */
+public abstract class SandboxedSdkProvider {
+ private Context mContext;
+ private SdkSandboxController mSdkSandboxController;
+
+ /**
+ * Sets the SDK {@link Context} which can then be received using {@link
+ * SandboxedSdkProvider#getContext()}. This is called before {@link
+ * SandboxedSdkProvider#onLoadSdk} is invoked. No operations requiring a {@link Context} should
+ * be performed before then, as {@link SandboxedSdkProvider#getContext} will return null until
+ * this method has been called.
+ *
+ * <p>Throws IllegalStateException if a base context has already been set.
+ *
+ * @param context The new base context.
+ */
+ public final void attachContext(@NonNull Context context) {
+ if (mContext != null) {
+ throw new IllegalStateException("Context already set");
+ }
+ Objects.requireNonNull(context, "Context cannot be null");
+ mContext = context;
+ }
+
+ /**
+ * Return the {@link Context} previously set through {@link SandboxedSdkProvider#attachContext}.
+ * This will return null if no context has been previously set.
+ */
+ @Nullable
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Does the work needed for the SDK to start handling requests.
+ *
+ * <p>This function is called by the SDK sandbox after it loads the SDK.
+ *
+ * <p>SDK should do any work to be ready to handle upcoming requests. It should not do any
+ * long-running tasks here, like I/O and network calls. Doing so can prevent the SDK from
+ * receiving requests from the client. Additionally, it should not do initialization that
+ * depends on other SDKs being loaded into the SDK sandbox.
+ *
+ * <p>The SDK should not do any operations requiring a {@link Context} object before this method
+ * has been called.
+ *
+ * @param params list of params passed from the client when it loads the SDK. This can be empty.
+ * @return Returns a {@link SandboxedSdk}, passed back to the client. The IBinder used to create
+ * the {@link SandboxedSdk} object will be used by the client to call into the SDK.
+ */
+ public abstract @NonNull SandboxedSdk onLoadSdk(@NonNull Bundle params) throws LoadSdkException;
+ /**
+ * Does the work needed for the SDK to free its resources before being unloaded.
+ *
+ * <p>This function is called by the SDK sandbox manager before it unloads the SDK. The SDK
+ * should fail any invocations on the Binder previously returned to the client through {@link
+ * SandboxedSdk#getInterface}.
+ *
+ * <p>The SDK should not do any long-running tasks here, like I/O and network calls.
+ */
+ public void beforeUnloadSdk() {}
+
+ /**
+ * Requests a view to be remotely rendered to the client app process.
+ *
+ * <p>Returns {@link View} will be wrapped into {@link SurfacePackage}. the resulting {@link
+ * SurfacePackage} will be sent back to the client application.
+ *
+ * <p>The SDK should not do any long-running tasks here, like I/O and network calls. Doing so
+ * can prevent the SDK from receiving requests from the client.
+ *
+ * @param windowContext the {@link Context} of the display which meant to show the view
+ * @param params list of params passed from the client application requesting the view
+ * @param width The view returned will be laid as if in a window of this width, in pixels.
+ * @param height The view returned will be laid as if in a window of this height, in pixels.
+ * @return a {@link View} which SDK sandbox pass to the client application requesting the view
+ */
+ @NonNull
+ public abstract View getView(
+ @NonNull Context windowContext, @NonNull Bundle params, int width, int height);
+}
diff --git a/android-34/android/app/sdksandbox/SdkSandboxLocalSingleton.java b/android-34/android/app/sdksandbox/SdkSandboxLocalSingleton.java
new file mode 100644
index 0000000..09600c4
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SdkSandboxLocalSingleton.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+
+/**
+ * Singleton for a privacy sandbox, which is initialised when the sandbox is created.
+ *
+ * @hide
+ */
+public class SdkSandboxLocalSingleton {
+
+ private static final String TAG = "SandboxLocalSingleton";
+ private static SdkSandboxLocalSingleton sInstance = null;
+ private final ISdkToServiceCallback mSdkToServiceCallback;
+
+ private SdkSandboxLocalSingleton(ISdkToServiceCallback sdkToServiceCallback) {
+ mSdkToServiceCallback = sdkToServiceCallback;
+ }
+
+ /**
+ * Returns a singleton instance of this class. TODO(b/247313241): Fix parameter once aidl issues
+ * are fixed.
+ *
+ * @param sdkToServiceBinder callback to support communication with the {@link
+ * com.android.server.sdksandbox.SdkSandboxManagerService}
+ * @throws IllegalStateException if singleton is already initialised
+ * @throws UnsupportedOperationException if the interface passed is not of type {@link
+ * ISdkToServiceCallback}
+ */
+ public static synchronized void initInstance(@NonNull IBinder sdkToServiceBinder) {
+ if (sInstance != null) {
+ Log.d(TAG, "Already Initialised");
+ return;
+ }
+ try {
+ if (Objects.nonNull(sdkToServiceBinder)
+ && sdkToServiceBinder
+ .getInterfaceDescriptor()
+ .equals(ISdkToServiceCallback.DESCRIPTOR)) {
+ sInstance =
+ new SdkSandboxLocalSingleton(
+ ISdkToServiceCallback.Stub.asInterface(sdkToServiceBinder));
+ return;
+ }
+ } catch (RemoteException e) {
+ // Fall through to the failure case.
+ }
+ throw new UnsupportedOperationException("IBinder not supported");
+ }
+
+ /** Returns an already initialised singleton instance of this class. */
+ public static SdkSandboxLocalSingleton getExistingInstance() {
+ if (sInstance == null) {
+ throw new IllegalStateException("SdkSandboxLocalSingleton not found");
+ }
+ return sInstance;
+ }
+
+ /** To reset the singleton. Only for Testing. */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public static void destroySingleton() {
+ sInstance = null;
+ }
+
+ /** Gets the callback to the {@link com.android.server.sdksandbox.SdkSandboxManagerService} */
+ public ISdkToServiceCallback getSdkToServiceCallback() {
+ return mSdkToServiceCallback;
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SdkSandboxManager.java b/android-34/android/app/sdksandbox/SdkSandboxManager.java
new file mode 100644
index 0000000..219601f
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SdkSandboxManager.java
@@ -0,0 +1,788 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import static android.app.sdksandbox.SdkSandboxManager.SDK_SANDBOX_SERVICE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.app.Activity;
+import android.app.sdksandbox.sdkprovider.SdkSandboxActivityHandler;
+import android.app.sdksandbox.sdkprovider.SdkSandboxController;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Provides APIs to load {@link android.content.pm.SharedLibraryInfo#TYPE_SDK_PACKAGE SDKs} into the
+ * SDK sandbox process, and then interact with them.
+ *
+ * <p>SDK sandbox is a java process running in a separate uid range. Each app may have its own SDK
+ * sandbox process.
+ *
+ * <p>The app first needs to declare SDKs it depends on in its manifest using the {@code
+ * <uses-sdk-library>} tag. Apps may only load SDKs they depend on into the SDK sandbox.
+ *
+ * @see android.content.pm.SharedLibraryInfo#TYPE_SDK_PACKAGE
+ * @see <a href="https://developer.android.com/design-for-safety/ads/sdk-runtime">SDK Runtime design
+ * proposal</a>
+ */
+@SystemService(SDK_SANDBOX_SERVICE)
+public final class SdkSandboxManager {
+
+ /**
+ * Use with {@link Context#getSystemService(String)} to retrieve an {@link SdkSandboxManager}
+ * for interacting with the SDKs belonging to this client application.
+ */
+ public static final String SDK_SANDBOX_SERVICE = "sdk_sandbox";
+
+ /**
+ * SDK sandbox process is not available.
+ *
+ * <p>This indicates that the SDK sandbox process is not available, either because it has died,
+ * disconnected or was not created in the first place.
+ */
+ public static final int SDK_SANDBOX_PROCESS_NOT_AVAILABLE = 503;
+
+ /**
+ * SDK not found.
+ *
+ * <p>This indicates that client application tried to load a non-existing SDK by calling {@link
+ * SdkSandboxManager#loadSdk(String, Bundle, Executor, OutcomeReceiver)}.
+ */
+ public static final int LOAD_SDK_NOT_FOUND = 100;
+
+ /**
+ * SDK is already loaded.
+ *
+ * <p>This indicates that client application tried to reload the same SDK by calling {@link
+ * SdkSandboxManager#loadSdk(String, Bundle, Executor, OutcomeReceiver)} after being
+ * successfully loaded.
+ */
+ public static final int LOAD_SDK_ALREADY_LOADED = 101;
+
+ /**
+ * SDK error after being loaded.
+ *
+ * <p>This indicates that the SDK encountered an error during post-load initialization. The
+ * details of this can be obtained from the Bundle returned in {@link LoadSdkException} through
+ * the {@link OutcomeReceiver} passed in to {@link SdkSandboxManager#loadSdk}.
+ */
+ public static final int LOAD_SDK_SDK_DEFINED_ERROR = 102;
+
+ /**
+ * SDK sandbox is disabled.
+ *
+ * <p>This indicates that the SDK sandbox is disabled. Any subsequent attempts to load SDKs in
+ * this boot will also fail.
+ */
+ public static final int LOAD_SDK_SDK_SANDBOX_DISABLED = 103;
+
+ /**
+ * Internal error while loading SDK.
+ *
+ * <p>This indicates a generic internal error happened while applying the call from client
+ * application.
+ */
+ public static final int LOAD_SDK_INTERNAL_ERROR = 500;
+
+ /**
+ * Action name for the intent which starts {@link Activity} in SDK sandbox.
+ *
+ * <p>System services would know if the intent is created to start {@link Activity} in sandbox
+ * by comparing the action of the intent to the value of this field.
+ *
+ * <p>This intent should contain an extra param with key equals to {@link
+ * #EXTRA_SANDBOXED_ACTIVITY_HANDLER} and value equals to the {@link IBinder} that identifies
+ * the {@link SdkSandboxActivityHandler} that registered before by an SDK. If the extra param is
+ * missing, the {@link Activity} will fail to start.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static final String ACTION_START_SANDBOXED_ACTIVITY =
+ "android.app.sdksandbox.action.START_SANDBOXED_ACTIVITY";
+
+ /**
+ * The key for an element in {@link Activity} intent extra params, the value is an {@link
+ * SdkSandboxActivityHandler} registered by an SDK.
+ *
+ * @hide
+ */
+ public static final String EXTRA_SANDBOXED_ACTIVITY_HANDLER =
+ "android.app.sdksandbox.extra.SANDBOXED_ACTIVITY_HANDLER";
+
+ private static final String TAG = "SdkSandboxManager";
+
+ /** @hide */
+ @IntDef(
+ value = {
+ LOAD_SDK_NOT_FOUND,
+ LOAD_SDK_ALREADY_LOADED,
+ LOAD_SDK_SDK_DEFINED_ERROR,
+ LOAD_SDK_SDK_SANDBOX_DISABLED,
+ LOAD_SDK_INTERNAL_ERROR,
+ SDK_SANDBOX_PROCESS_NOT_AVAILABLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LoadSdkErrorCode {}
+
+ /** Internal error while requesting a {@link SurfacePackage}.
+ *
+ * <p>This indicates a generic internal error happened while requesting a
+ * {@link SurfacePackage}.
+ */
+ public static final int REQUEST_SURFACE_PACKAGE_INTERNAL_ERROR = 700;
+
+ /**
+ * SDK is not loaded while requesting a {@link SurfacePackage}.
+ *
+ * <p>This indicates that the SDK for which the {@link SurfacePackage} is being requested is not
+ * loaded, either because the sandbox died or because it was not loaded in the first place.
+ */
+ public static final int REQUEST_SURFACE_PACKAGE_SDK_NOT_LOADED = 701;
+
+ /** @hide */
+ @IntDef(
+ prefix = "REQUEST_SURFACE_PACKAGE_",
+ value = {
+ REQUEST_SURFACE_PACKAGE_INTERNAL_ERROR,
+ REQUEST_SURFACE_PACKAGE_SDK_NOT_LOADED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RequestSurfacePackageErrorCode {}
+
+ /**
+ * SDK sandbox is disabled.
+ *
+ * <p>{@link SdkSandboxManager} APIs are hidden. Attempts at calling them will result in {@link
+ * UnsupportedOperationException}.
+ */
+ public static final int SDK_SANDBOX_STATE_DISABLED = 0;
+
+ /**
+ * SDK sandbox is enabled.
+ *
+ * <p>App can use {@link SdkSandboxManager} APIs to load {@code SDKs} it depends on into the
+ * corresponding SDK sandbox process.
+ */
+ public static final int SDK_SANDBOX_STATE_ENABLED_PROCESS_ISOLATION = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "SDK_SANDBOX_STATUS_", value = {
+ SDK_SANDBOX_STATE_DISABLED,
+ SDK_SANDBOX_STATE_ENABLED_PROCESS_ISOLATION,
+ })
+ public @interface SdkSandboxState {}
+
+ /**
+ * The name of key to be used in the Bundle fields of {@link #requestSurfacePackage(String,
+ * Bundle, Executor, OutcomeReceiver)}, its value should define the integer width of the {@link
+ * SurfacePackage} in pixels.
+ */
+ public static final String EXTRA_WIDTH_IN_PIXELS =
+ "android.app.sdksandbox.extra.WIDTH_IN_PIXELS";
+ /**
+ * The name of key to be used in the Bundle fields of {@link #requestSurfacePackage(String,
+ * Bundle, Executor, OutcomeReceiver)}, its value should define the integer height of the {@link
+ * SurfacePackage} in pixels.
+ */
+ public static final String EXTRA_HEIGHT_IN_PIXELS =
+ "android.app.sdksandbox.extra.HEIGHT_IN_PIXELS";
+ /**
+ * The name of key to be used in the Bundle fields of {@link #requestSurfacePackage(String,
+ * Bundle, Executor, OutcomeReceiver)}, its value should define the integer ID of the logical
+ * display to display the {@link SurfacePackage}.
+ */
+ public static final String EXTRA_DISPLAY_ID = "android.app.sdksandbox.extra.DISPLAY_ID";
+
+ /**
+ * The name of key to be used in the Bundle fields of {@link #requestSurfacePackage(String,
+ * Bundle, Executor, OutcomeReceiver)}, its value should present the token returned by {@link
+ * android.view.SurfaceView#getHostToken()} once the {@link android.view.SurfaceView} has been
+ * added to the view hierarchy. Only a non-null value is accepted to enable ANR reporting.
+ */
+ public static final String EXTRA_HOST_TOKEN = "android.app.sdksandbox.extra.HOST_TOKEN";
+
+ /**
+ * The name of key in the Bundle which is passed to the {@code onResult} function of the {@link
+ * OutcomeReceiver} which is field of {@link #requestSurfacePackage(String, Bundle, Executor,
+ * OutcomeReceiver)}, its value presents the requested {@link SurfacePackage}.
+ */
+ public static final String EXTRA_SURFACE_PACKAGE =
+ "android.app.sdksandbox.extra.SURFACE_PACKAGE";
+
+ private final ISdkSandboxManager mService;
+ private final Context mContext;
+
+ @GuardedBy("mLifecycleCallbacks")
+ private final ArrayList<SdkSandboxProcessDeathCallbackProxy> mLifecycleCallbacks =
+ new ArrayList<>();
+
+ private final SharedPreferencesSyncManager mSyncManager;
+
+ /** @hide */
+ public SdkSandboxManager(@NonNull Context context, @NonNull ISdkSandboxManager binder) {
+ mContext = Objects.requireNonNull(context, "context should not be null");
+ mService = Objects.requireNonNull(binder, "binder should not be null");
+ // TODO(b/239403323): There can be multiple package in the same app process
+ mSyncManager = SharedPreferencesSyncManager.getInstance(context, binder);
+ }
+
+ /** Returns the current state of the availability of the SDK sandbox feature. */
+ @SdkSandboxState
+ public static int getSdkSandboxState() {
+ return SDK_SANDBOX_STATE_ENABLED_PROCESS_ISOLATION;
+ }
+
+ /**
+ * Stops the SDK sandbox process corresponding to the app.
+ *
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission("com.android.app.sdksandbox.permission.STOP_SDK_SANDBOX")
+ public void stopSdkSandbox() {
+ try {
+ mService.stopSdkSandbox(mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Adds a callback which gets registered for SDK sandbox lifecycle events, such as SDK sandbox
+ * death. If the sandbox has not yet been created when this is called, the request will be
+ * stored until a sandbox is created, at which point it is activated for that sandbox. Multiple
+ * callbacks can be added to detect death and will not be removed when the sandbox dies.
+ *
+ * @param callbackExecutor the {@link Executor} on which to invoke the callback
+ * @param callback the {@link SdkSandboxProcessDeathCallback} which will receive SDK sandbox
+ * lifecycle events.
+ */
+ public void addSdkSandboxProcessDeathCallback(
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull SdkSandboxProcessDeathCallback callback) {
+ Objects.requireNonNull(callbackExecutor, "callbackExecutor should not be null");
+ Objects.requireNonNull(callback, "callback should not be null");
+
+ synchronized (mLifecycleCallbacks) {
+ final SdkSandboxProcessDeathCallbackProxy callbackProxy =
+ new SdkSandboxProcessDeathCallbackProxy(callbackExecutor, callback);
+ try {
+ mService.addSdkSandboxProcessDeathCallback(
+ mContext.getPackageName(),
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis(),
+ callbackProxy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mLifecycleCallbacks.add(callbackProxy);
+ }
+ }
+
+ /**
+ * Removes an {@link SdkSandboxProcessDeathCallback} that was previously added using {@link
+ * SdkSandboxManager#addSdkSandboxProcessDeathCallback(Executor,
+ * SdkSandboxProcessDeathCallback)}
+ *
+ * @param callback the {@link SdkSandboxProcessDeathCallback} which was previously added using
+ * {@link SdkSandboxManager#addSdkSandboxProcessDeathCallback(Executor,
+ * SdkSandboxProcessDeathCallback)}
+ */
+ public void removeSdkSandboxProcessDeathCallback(
+ @NonNull SdkSandboxProcessDeathCallback callback) {
+ Objects.requireNonNull(callback, "callback should not be null");
+ synchronized (mLifecycleCallbacks) {
+ for (int i = mLifecycleCallbacks.size() - 1; i >= 0; i--) {
+ final SdkSandboxProcessDeathCallbackProxy callbackProxy =
+ mLifecycleCallbacks.get(i);
+ if (callbackProxy.callback == callback) {
+ try {
+ mService.removeSdkSandboxProcessDeathCallback(
+ mContext.getPackageName(),
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis(),
+ callbackProxy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mLifecycleCallbacks.remove(i);
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads SDK in an SDK sandbox java process.
+ *
+ * <p>Loads SDK library with {@code sdkName} to an SDK sandbox process asynchronously. The
+ * caller will be notified through the {@code receiver}.
+ *
+ * <p>The caller should already declare {@code SDKs} it depends on in its manifest using {@code
+ * <uses-sdk-library>} tag. The caller may only load {@code SDKs} it depends on into the SDK
+ * sandbox.
+ *
+ * <p>When the client application loads the first SDK, a new SDK sandbox process will be
+ * created. If a sandbox has already been created for the client application, additional SDKs
+ * will be loaded into the same sandbox.
+ *
+ * <p>This API may only be called while the caller is running in the foreground. Calls from the
+ * background will result in returning {@link LoadSdkException} in the {@code receiver}.
+ *
+ * @param sdkName name of the SDK to be loaded.
+ * @param params additional parameters to be passed to the SDK in the form of a {@link Bundle}
+ * as agreed between the client and the SDK.
+ * @param executor the {@link Executor} on which to invoke the receiver.
+ * @param receiver This either receives a {@link SandboxedSdk} on a successful run, or {@link
+ * LoadSdkException}.
+ */
+ public void loadSdk(
+ @NonNull String sdkName,
+ @NonNull Bundle params,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<SandboxedSdk, LoadSdkException> receiver) {
+ Objects.requireNonNull(sdkName, "sdkName should not be null");
+ Objects.requireNonNull(params, "params should not be null");
+ Objects.requireNonNull(executor, "executor should not be null");
+ Objects.requireNonNull(receiver, "receiver should not be null");
+ final LoadSdkReceiverProxy callbackProxy =
+ new LoadSdkReceiverProxy(executor, receiver, mService);
+
+ IBinder appProcessToken;
+ // Context.getProcessToken() only exists on U+.
+ if (SdkLevel.isAtLeastU()) {
+ appProcessToken = mContext.getProcessToken();
+ } else {
+ appProcessToken = null;
+ }
+ try {
+ mService.loadSdk(
+ mContext.getPackageName(),
+ appProcessToken,
+ sdkName,
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis(),
+ params,
+ callbackProxy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Fetches information about SDKs that are loaded in the sandbox.
+ *
+ * @return List of {@link SandboxedSdk} containing all currently loaded SDKs.
+ */
+ public @NonNull List<SandboxedSdk> getSandboxedSdks() {
+ try {
+ return mService.getSandboxedSdks(
+ mContext.getPackageName(),
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unloads an SDK that has been previously loaded by the caller.
+ *
+ * <p>It is not guaranteed that the memory allocated for this SDK will be freed immediately. All
+ * subsequent calls to {@link #requestSurfacePackage(String, Bundle, Executor, OutcomeReceiver)}
+ * for the given {@code sdkName} will fail.
+ *
+ * <p>This API may only be called while the caller is running in the foreground. Calls from the
+ * background will result in a {@link SecurityException} being thrown.
+ *
+ * @param sdkName name of the SDK to be unloaded.
+ */
+ public void unloadSdk(@NonNull String sdkName) {
+ Objects.requireNonNull(sdkName, "sdkName should not be null");
+ try {
+ mService.unloadSdk(
+ mContext.getPackageName(),
+ sdkName,
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sends a request for a surface package to the SDK.
+ *
+ * <p>After the client application receives a signal about a successful SDK loading, and has
+ * added a {@link android.view.SurfaceView} to the view hierarchy, it may asynchronously request
+ * a {@link SurfacePackage} to render a view from the SDK.
+ *
+ * <p>When the {@link SurfacePackage} is ready, the {@link OutcomeReceiver#onResult} callback of
+ * the passed {@code receiver} will be invoked. This callback will contain a {@link Bundle}
+ * object, which will contain the key {@link SdkSandboxManager#EXTRA_SURFACE_PACKAGE} whose
+ * associated value is the requested {@link SurfacePackage}.
+ *
+ * <p>The passed {@code params} must contain the following keys: {@link
+ * SdkSandboxManager#EXTRA_WIDTH_IN_PIXELS}, {@link SdkSandboxManager#EXTRA_HEIGHT_IN_PIXELS},
+ * {@link SdkSandboxManager#EXTRA_DISPLAY_ID} and {@link SdkSandboxManager#EXTRA_HOST_TOKEN}. If
+ * any of these keys are missing or invalid, an {@link IllegalArgumentException} will be thrown.
+ *
+ * <p>This API may only be called while the caller is running in the foreground. Calls from the
+ * background will result in returning RequestSurfacePackageException in the {@code receiver}.
+ *
+ * @param sdkName name of the SDK loaded into the SDK sandbox.
+ * @param params the parameters which the client application passes to the SDK.
+ * @param callbackExecutor the {@link Executor} on which to invoke the callback
+ * @param receiver This either returns a {@link Bundle} on success which will contain the key
+ * {@link SdkSandboxManager#EXTRA_SURFACE_PACKAGE} with a {@link SurfacePackage} value, or
+ * {@link RequestSurfacePackageException} on failure.
+ * @throws IllegalArgumentException if {@code params} does not contain all required keys.
+ * @see android.app.sdksandbox.SdkSandboxManager#EXTRA_WIDTH_IN_PIXELS
+ * @see android.app.sdksandbox.SdkSandboxManager#EXTRA_HEIGHT_IN_PIXELS
+ * @see android.app.sdksandbox.SdkSandboxManager#EXTRA_DISPLAY_ID
+ * @see android.app.sdksandbox.SdkSandboxManager#EXTRA_HOST_TOKEN
+ */
+ public void requestSurfacePackage(
+ @NonNull String sdkName,
+ @NonNull Bundle params,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull OutcomeReceiver<Bundle, RequestSurfacePackageException> receiver) {
+ Objects.requireNonNull(sdkName, "sdkName should not be null");
+ Objects.requireNonNull(params, "params should not be null");
+ Objects.requireNonNull(callbackExecutor, "callbackExecutor should not be null");
+ Objects.requireNonNull(receiver, "receiver should not be null");
+ try {
+ int width = params.getInt(EXTRA_WIDTH_IN_PIXELS, -1); // -1 means invalid width
+ if (width <= 0) {
+ throw new IllegalArgumentException(
+ "Field params should have the entry for the key ("
+ + EXTRA_WIDTH_IN_PIXELS
+ + ") with positive integer value");
+ }
+
+ int height = params.getInt(EXTRA_HEIGHT_IN_PIXELS, -1); // -1 means invalid height
+ if (height <= 0) {
+ throw new IllegalArgumentException(
+ "Field params should have the entry for the key ("
+ + EXTRA_HEIGHT_IN_PIXELS
+ + ") with positive integer value");
+ }
+
+ int displayId = params.getInt(EXTRA_DISPLAY_ID, -1); // -1 means invalid displayId
+ if (displayId < 0) {
+ throw new IllegalArgumentException(
+ "Field params should have the entry for the key ("
+ + EXTRA_DISPLAY_ID
+ + ") with integer >= 0");
+ }
+
+ IBinder hostToken = params.getBinder(EXTRA_HOST_TOKEN);
+ if (hostToken == null) {
+ throw new IllegalArgumentException(
+ "Field params should have the entry for the key ("
+ + EXTRA_HOST_TOKEN
+ + ") with not null IBinder value");
+ }
+
+ final RequestSurfacePackageReceiverProxy callbackProxy =
+ new RequestSurfacePackageReceiverProxy(callbackExecutor, receiver, mService);
+
+ mService.requestSurfacePackage(
+ mContext.getPackageName(),
+ sdkName,
+ hostToken,
+ displayId,
+ width,
+ height,
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis(),
+ params,
+ callbackProxy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Starts an {@link Activity} in the SDK sandbox.
+ *
+ * <p>This function will start a new {@link Activity} in the same task of the passed {@code
+ * fromActivity} and pass it to the SDK that shared the passed {@code sdkActivityToken} that
+ * identifies a request from that SDK to stat this {@link Activity}.
+ *
+ * <p>The {@link Activity} will not start in the following cases:
+ *
+ * <ul>
+ * <li>The App calling this API is in the background.
+ * <li>The passed {@code sdkActivityToken} does not map to a request for an {@link Activity}
+ * form the SDK that shared it with the caller app.
+ * <li>The SDK that shared the passed {@code sdkActivityToken} removed its request for this
+ * {@link Activity}.
+ * <li>The sandbox {@link Activity} is already created.
+ * </ul>
+ *
+ * @param fromActivity the {@link Activity} will be used to start the new sandbox {@link
+ * Activity} by calling {@link Activity#startActivity(Intent)} against it.
+ * @param sdkActivityToken the identifier that is shared by the SDK which requests the {@link
+ * Activity}.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void startSdkSandboxActivity(
+ @NonNull Activity fromActivity, @NonNull IBinder sdkActivityToken) {
+ if (!SdkLevel.isAtLeastU()) {
+ throw new UnsupportedOperationException();
+ }
+ Intent intent = new Intent();
+ intent.setAction(ACTION_START_SANDBOXED_ACTIVITY);
+ intent.setPackage(mContext.getPackageManager().getSdkSandboxPackageName());
+
+ Bundle params = new Bundle();
+ params.putBinder(EXTRA_SANDBOXED_ACTIVITY_HANDLER, sdkActivityToken);
+ intent.putExtras(params);
+
+ fromActivity.startActivity(intent);
+ }
+
+ /**
+ * A callback for tracking events SDK sandbox death.
+ *
+ * <p>The callback can be added using {@link
+ * SdkSandboxManager#addSdkSandboxProcessDeathCallback(Executor,
+ * SdkSandboxProcessDeathCallback)} and removed using {@link
+ * SdkSandboxManager#removeSdkSandboxProcessDeathCallback(SdkSandboxProcessDeathCallback)}
+ */
+ public interface SdkSandboxProcessDeathCallback {
+ /**
+ * Notifies the client application that the SDK sandbox has died. The sandbox could die for
+ * various reasons, for example, due to memory pressure on the system, or a crash in the
+ * sandbox.
+ *
+ * The system will automatically restart the sandbox process if it died due to a crash.
+ * However, the state of the sandbox will be lost - so any SDKs that were loaded previously
+ * would have to be loaded again, using {@link SdkSandboxManager#loadSdk(String, Bundle,
+ * Executor, OutcomeReceiver)} to continue using them.
+ */
+ void onSdkSandboxDied();
+ }
+
+ /** @hide */
+ private static class SdkSandboxProcessDeathCallbackProxy
+ extends ISdkSandboxProcessDeathCallback.Stub {
+ private final Executor mExecutor;
+ public final SdkSandboxProcessDeathCallback callback;
+
+ SdkSandboxProcessDeathCallbackProxy(
+ Executor executor, SdkSandboxProcessDeathCallback lifecycleCallback) {
+ mExecutor = executor;
+ callback = lifecycleCallback;
+ }
+
+ @Override
+ public void onSdkSandboxDied() {
+ mExecutor.execute(() -> callback.onSdkSandboxDied());
+ }
+ }
+
+ /**
+ * Adds keys to set of keys being synced from app's default {@link SharedPreferences} to the SDK
+ * sandbox.
+ *
+ * <p>Synced data will be available for SDKs to read using the {@link
+ * SdkSandboxController#getClientSharedPreferences()} API.
+ *
+ * <p>To stop syncing any key that has been added using this API, use {@link
+ * #removeSyncedSharedPreferencesKeys(Set)}.
+ *
+ * <p>The sync breaks if the app restarts and user must call this API again to rebuild the pool
+ * of keys for syncing.
+ *
+ * <p>Note: This class does not support use across multiple processes.
+ *
+ * @param keys set of keys that will be synced to Sandbox.
+ */
+ public void addSyncedSharedPreferencesKeys(@NonNull Set<String> keys) {
+ Objects.requireNonNull(keys, "keys cannot be null");
+ for (String key : keys) {
+ if (key == null) {
+ throw new IllegalArgumentException("keys cannot contain null");
+ }
+ }
+ mSyncManager.addSharedPreferencesSyncKeys(keys);
+ }
+
+ /**
+ * Removes keys from set of keys that have been added using {@link
+ * #addSyncedSharedPreferencesKeys(Set)}
+ *
+ * <p>Removed keys will be erased from the SDK sandbox if they have been synced already.
+ *
+ * @param keys set of key names that should no longer be synced to Sandbox.
+ */
+ public void removeSyncedSharedPreferencesKeys(@NonNull Set<String> keys) {
+ for (String key : keys) {
+ if (key == null) {
+ throw new IllegalArgumentException("keys cannot contain null");
+ }
+ }
+ mSyncManager.removeSharedPreferencesSyncKeys(keys);
+ }
+
+ /**
+ * Returns the set keys that are being synced from app's default {@link SharedPreferences} to
+ * the SDK sandbox.
+ */
+ @NonNull
+ public Set<String> getSyncedSharedPreferencesKeys() {
+ return mSyncManager.getSharedPreferencesSyncKeys();
+ }
+
+ /** @hide */
+ private static class LoadSdkReceiverProxy extends ILoadSdkCallback.Stub {
+ private final Executor mExecutor;
+ private final OutcomeReceiver<SandboxedSdk, LoadSdkException> mCallback;
+ private final ISdkSandboxManager mService;
+
+ LoadSdkReceiverProxy(
+ Executor executor,
+ OutcomeReceiver<SandboxedSdk, LoadSdkException> callback,
+ ISdkSandboxManager service) {
+ mExecutor = executor;
+ mCallback = callback;
+ mService = service;
+ }
+
+ @Override
+ public void onLoadSdkSuccess(SandboxedSdk sandboxedSdk, long timeSystemServerCalledApp) {
+ logLatencyFromSystemServerToApp(timeSystemServerCalledApp);
+ mExecutor.execute(() -> mCallback.onResult(sandboxedSdk));
+ }
+
+ @Override
+ public void onLoadSdkFailure(LoadSdkException exception, long timeSystemServerCalledApp) {
+ logLatencyFromSystemServerToApp(timeSystemServerCalledApp);
+ mExecutor.execute(() -> mCallback.onError(exception));
+ }
+
+ private void logLatencyFromSystemServerToApp(long timeSystemServerCalledApp) {
+ try {
+ mService.logLatencyFromSystemServerToApp(
+ ISdkSandboxManager.LOAD_SDK,
+ // TODO(b/242832156): Add Injector class for testing
+ (int) (System.currentTimeMillis() - timeSystemServerCalledApp));
+ } catch (RemoteException e) {
+ Log.w(
+ TAG,
+ "Remote exception while calling logLatencyFromSystemServerToApp."
+ + "Error: "
+ + e.getMessage());
+ }
+ }
+ }
+
+ /** @hide */
+ private static class RequestSurfacePackageReceiverProxy
+ extends IRequestSurfacePackageCallback.Stub {
+ private final Executor mExecutor;
+ private final OutcomeReceiver<Bundle, RequestSurfacePackageException> mReceiver;
+ private final ISdkSandboxManager mService;
+
+ RequestSurfacePackageReceiverProxy(
+ Executor executor,
+ OutcomeReceiver<Bundle, RequestSurfacePackageException> receiver,
+ ISdkSandboxManager service) {
+ mExecutor = executor;
+ mReceiver = receiver;
+ mService = service;
+ }
+
+ @Override
+ public void onSurfacePackageReady(
+ SurfacePackage surfacePackage,
+ int surfacePackageId,
+ Bundle params,
+ long timeSystemServerCalledApp) {
+ logLatencyFromSystemServerToApp(timeSystemServerCalledApp);
+ mExecutor.execute(
+ () -> {
+ params.putParcelable(EXTRA_SURFACE_PACKAGE, surfacePackage);
+ mReceiver.onResult(params);
+ });
+ }
+
+ @Override
+ public void onSurfacePackageError(
+ int errorCode, String errorMsg, long timeSystemServerCalledApp) {
+ logLatencyFromSystemServerToApp(timeSystemServerCalledApp);
+ mExecutor.execute(
+ () ->
+ mReceiver.onError(
+ new RequestSurfacePackageException(errorCode, errorMsg)));
+ }
+
+ private void logLatencyFromSystemServerToApp(long timeSystemServerCalledApp) {
+ try {
+ mService.logLatencyFromSystemServerToApp(
+ ISdkSandboxManager.REQUEST_SURFACE_PACKAGE,
+ // TODO(b/242832156): Add Injector class for testing
+ (int) (System.currentTimeMillis() - timeSystemServerCalledApp));
+ } catch (RemoteException e) {
+ Log.w(
+ TAG,
+ "Remote exception while calling logLatencyFromSystemServerToApp."
+ + "Error: "
+ + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Return the AdServicesManager
+ *
+ * @hide
+ */
+ public IBinder getAdServicesManager() {
+ try {
+ return mService.getAdServicesManager();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SdkSandboxManagerFrameworkInitializer.java b/android-34/android/app/sdksandbox/SdkSandboxManagerFrameworkInitializer.java
new file mode 100644
index 0000000..37d44ad
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SdkSandboxManagerFrameworkInitializer.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import static android.app.sdksandbox.SdkSandboxManager.SDK_SANDBOX_SERVICE;
+import static android.app.sdksandbox.sdkprovider.SdkSandboxController.SDK_SANDBOX_CONTROLLER_SERVICE;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.app.sdksandbox.sdkprovider.SdkSandboxController;
+import android.content.Context;
+
+/**
+ * Class holding initialization code for all Sandbox Runtime system services.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class SdkSandboxManagerFrameworkInitializer {
+ private SdkSandboxManagerFrameworkInitializer() {
+ }
+
+ /**
+ * Called by {@link SystemServiceRegistry}'s static initializer and registers all sandbox
+ * runtime services to {@link Context}, so that {@link Context#getSystemService} can return
+ * them.
+ *
+ * @throws IllegalStateException if this is called from anywhere besides {@link
+ * SystemServiceRegistry}
+ */
+ public static void registerServiceWrappers() {
+ SystemServiceRegistry.registerContextAwareService(
+ SDK_SANDBOX_SERVICE, SdkSandboxManager.class,
+ (context, service) -> new SdkSandboxManager(
+ context, ISdkSandboxManager.Stub.asInterface(service))
+ );
+
+ SystemServiceRegistry.registerContextAwareService(
+ SDK_SANDBOX_CONTROLLER_SERVICE,
+ SdkSandboxController.class,
+ (context) -> new SdkSandboxController(context));
+ // TODO(b/242889021): don't use this workaround on devices that have proper fix
+ SdkSandboxSystemServiceRegistry.getInstance()
+ .registerServiceMutator(
+ SDK_SANDBOX_CONTROLLER_SERVICE,
+ (service, context) -> ((SdkSandboxController) service).initialize(context));
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SdkSandboxSystemServiceRegistry.java b/android-34/android/app/sdksandbox/SdkSandboxSystemServiceRegistry.java
new file mode 100644
index 0000000..7f30146
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SdkSandboxSystemServiceRegistry.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Map;
+
+/**
+ * Maintains a set of services which {@code Manager} classes should have a {@link
+ * SandboxedSdkContext} in case they are running inside a {@code sdk_sandbox} process.
+ *
+ * <p>This class is required to work around the fact that {@link
+ * android.content.ContextWrapper#getSystemService(String)} delegates the call to the base context,
+ * and reimplementing this method in the {@link SandboxedSdkContext} will require accessing hidden
+ * APIs, which is forbidden for Mainline modules.
+ *
+ * <p>Manager classes that want to behave differently in case they are initiated from inside a
+ * {@code sdk_sandbox} process are expected to call {@link #registerServiceMutator(String,
+ * ServiceMutator)} in their initialization code, e.g. {@link
+ * android.adservices.AdServicesFrameworkInitializer#registerServiceWrappers()}. When a {@code SDK}
+ * running inside a {@code sdk_sandbox} process requests a "sdk sandbox aware" manager via {@link
+ * SandboxedSdkContext#getSystemService(String)} the code inside {@link
+ * SandboxedSdkContext#getSystemService(String)} will use the registered {@link ServiceMutator} to
+ * set the correct context.
+ *
+ * @hide
+ */
+// TODO(b/242889021): limit this class only to Android T, on U+ we should implement the proper
+// platform support.
+public final class SdkSandboxSystemServiceRegistry {
+
+ @VisibleForTesting
+ public SdkSandboxSystemServiceRegistry() {}
+
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static SdkSandboxSystemServiceRegistry sInstance = null;
+
+ /** Returns an instance of {@link SdkSandboxSystemServiceRegistry}. */
+ @NonNull
+ public static SdkSandboxSystemServiceRegistry getInstance() {
+ synchronized (sLock) {
+ if (sInstance == null) {
+ sInstance = new SdkSandboxSystemServiceRegistry();
+ }
+ return sInstance;
+ }
+ }
+
+ @GuardedBy("mServiceMutators")
+ private final Map<String, ServiceMutator> mServiceMutators = new ArrayMap<>();
+
+ /**
+ * Adds a {@code mutator} for the service with given {@code serviceName}.
+ *
+ * <p>This {@code mutator} will be applied inside the {@link
+ * SandboxedSdkContext#getSystemService(String)} method.
+ */
+ public void registerServiceMutator(
+ @NonNull String serviceName, @NonNull ServiceMutator mutator) {
+ synchronized (mServiceMutators) {
+ mServiceMutators.put(serviceName, mutator);
+ }
+ }
+
+ /**
+ * Returns a {@link ServiceMutator} for the given {@code serviceName}, or {@code null} if this
+ * {@code serviceName} doesn't have a mutator registered.
+ */
+ @Nullable
+ public ServiceMutator getServiceMutator(@NonNull String serviceName) {
+ synchronized (mServiceMutators) {
+ return mServiceMutators.get(serviceName);
+ }
+ }
+
+ /**
+ * A functional interface representing a method on a {@code Manager} class to set the context.
+ *
+ * <p>This interface is required in order to break the circular dependency between {@code
+ * framework-sdsksandbox} and {@code framework-adservices} build targets.
+ */
+ public interface ServiceMutator {
+
+ /** Sets a {@code context} on the given {@code service}. */
+ @NonNull
+ Object setContext(@NonNull Object service, @NonNull Context context);
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SharedPreferencesKey.java b/android-34/android/app/sdksandbox/SharedPreferencesKey.java
new file mode 100644
index 0000000..6469f48
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SharedPreferencesKey.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Key with its type to be synced using {@link
+ * SdkSandboxManager#addSyncedSharedPreferencesKeys(java.util.Set)}
+ *
+ * @hide
+ */
+public final class SharedPreferencesKey implements Parcelable {
+ /** @hide */
+ @IntDef(
+ prefix = "KEY_TYPE_",
+ value = {
+ KEY_TYPE_BOOLEAN,
+ KEY_TYPE_FLOAT,
+ KEY_TYPE_INTEGER,
+ KEY_TYPE_LONG,
+ KEY_TYPE_STRING,
+ KEY_TYPE_STRING_SET,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface KeyType {}
+
+ /**
+ * Key type {@code Boolean}.
+ */
+ public static final int KEY_TYPE_BOOLEAN = 1;
+
+ /**
+ * Key type {@code Float}.
+ */
+ public static final int KEY_TYPE_FLOAT = 2;
+
+ /**
+ * Key type {@code Integer}.
+ */
+ public static final int KEY_TYPE_INTEGER = 3;
+
+ /**
+ * Key type {@code Long}.
+ */
+ public static final int KEY_TYPE_LONG = 4;
+
+ /**
+ * Key type {@code String}.
+ */
+ public static final int KEY_TYPE_STRING = 5;
+
+ /**
+ * Key type {@code Set<String>}.
+ */
+ public static final int KEY_TYPE_STRING_SET = 6;
+
+ private final String mKeyName;
+ private final @KeyType int mKeyType;
+
+ public static final @NonNull Parcelable.Creator<SharedPreferencesKey> CREATOR =
+ new Parcelable.Creator<SharedPreferencesKey>() {
+ public SharedPreferencesKey createFromParcel(Parcel in) {
+ return new SharedPreferencesKey(in);
+ }
+
+ public SharedPreferencesKey[] newArray(int size) {
+ return new SharedPreferencesKey[size];
+ }
+ };
+
+ public SharedPreferencesKey(@NonNull String keyName, @KeyType int keyType) {
+ mKeyName = keyName;
+ mKeyType = keyType;
+ }
+
+ private SharedPreferencesKey(Parcel in) {
+ mKeyName = in.readString();
+ mKeyType = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeString(mKeyName);
+ out.writeInt(mKeyType);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "SharedPreferencesKey{" + "mKeyName=" + mKeyName + ", mKeyType='" + mKeyType + "'}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SharedPreferencesKey)) return false;
+ final SharedPreferencesKey that = (SharedPreferencesKey) o;
+ return mKeyName.equals(that.getName()) && mKeyType == that.getType();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mKeyName, mKeyType);
+ }
+
+ /** Get name of the key. */
+ @NonNull
+ public String getName() {
+ return mKeyName;
+ }
+
+ /** Get type of the key */
+ public @KeyType int getType() {
+ return mKeyType;
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SharedPreferencesSyncManager.java b/android-34/android/app/sdksandbox/SharedPreferencesSyncManager.java
new file mode 100644
index 0000000..5e0c383
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SharedPreferencesSyncManager.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Syncs specified keys in default {@link SharedPreferences} to Sandbox.
+ *
+ * <p>This class is a singleton since we want to maintain sync between app process and sandbox
+ * process.
+ *
+ * @hide
+ */
+public class SharedPreferencesSyncManager {
+
+ private static final String TAG = "SdkSandboxSyncManager";
+ private static ArrayMap<String, SharedPreferencesSyncManager> sInstanceMap = new ArrayMap<>();
+ private final ISdkSandboxManager mService;
+ private final Context mContext;
+ private final Object mLock = new Object();
+ private final ISharedPreferencesSyncCallback mCallback = new SharedPreferencesSyncCallback();
+
+ @GuardedBy("mLock")
+ private boolean mWaitingForSandbox = false;
+
+ // Set to a listener after initial bulk sync is successful
+ @GuardedBy("mLock")
+ private ChangeListener mListener = null;
+
+ // Set of keys that this manager needs to keep in sync.
+ @GuardedBy("mLock")
+ private ArraySet<String> mKeysToSync = new ArraySet<>();
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public SharedPreferencesSyncManager(
+ @NonNull Context context, @NonNull ISdkSandboxManager service) {
+ mContext = context.getApplicationContext();
+ mService = service;
+ }
+
+ /**
+ * Returns a new instance of this class if there is a new package, otherewise returns a
+ * singleton instance.
+ */
+ public static synchronized SharedPreferencesSyncManager getInstance(
+ @NonNull Context context, @NonNull ISdkSandboxManager service) {
+ final String packageName = context.getPackageName();
+ if (!sInstanceMap.containsKey(packageName)) {
+ sInstanceMap.put(packageName, new SharedPreferencesSyncManager(context, service));
+ }
+ return sInstanceMap.get(packageName);
+ }
+
+ /**
+ * Adds keys for syncing from app's default {@link SharedPreferences} to SdkSandbox.
+ *
+ * @see SdkSandboxManager#addSyncedSharedPreferencesKeys(Set)
+ */
+ public void addSharedPreferencesSyncKeys(@NonNull Set<String> keyNames) {
+ // TODO(b/239403323): Validate the parameters in SdkSandboxManager
+ synchronized (mLock) {
+ mKeysToSync.addAll(keyNames);
+
+ if (mListener == null) {
+ mListener = new ChangeListener();
+ getDefaultSharedPreferences().registerOnSharedPreferenceChangeListener(mListener);
+ }
+ syncData();
+ }
+ }
+
+ /**
+ * Removes keys from set of keys that have been added using {@link
+ * #addSharedPreferencesSyncKeys(Set)}
+ *
+ * @see SdkSandboxManager#removeSyncedSharedPreferencesKeys(Set)
+ */
+ public void removeSharedPreferencesSyncKeys(@NonNull Set<String> keys) {
+ synchronized (mLock) {
+ mKeysToSync.removeAll(keys);
+
+ final ArrayList<SharedPreferencesKey> keysWithTypeBeingRemoved = new ArrayList<>();
+
+ for (final String key : keys) {
+ keysWithTypeBeingRemoved.add(
+ new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING));
+ }
+ final SharedPreferencesUpdate update =
+ new SharedPreferencesUpdate(keysWithTypeBeingRemoved, new Bundle());
+ try {
+ mService.syncDataFromClient(
+ mContext.getPackageName(),
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis(),
+ update,
+ mCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Returns the set of all keys that are being synced from app's default {@link
+ * SharedPreferences} to sandbox.
+ */
+ public Set<String> getSharedPreferencesSyncKeys() {
+ synchronized (mLock) {
+ return new ArraySet(mKeysToSync);
+ }
+ }
+
+ /**
+ * Returns true if sync is in waiting state.
+ *
+ * <p>Sync transitions into waiting state whenever sdksandbox is unavailable. It resumes syncing
+ * again when SdkSandboxManager notifies us that sdksandbox is available again.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public boolean isWaitingForSandbox() {
+ synchronized (mLock) {
+ return mWaitingForSandbox;
+ }
+ }
+
+ /**
+ * Syncs data to SdkSandbox.
+ *
+ * <p>Syncs values of specified keys {@link #mKeysToSync} from the default {@link
+ * SharedPreferences} of the app.
+ *
+ * <p>Once bulk sync is complete, it also registers listener for updates which maintains the
+ * sync.
+ */
+ private void syncData() {
+ synchronized (mLock) {
+ // Do not sync if keys have not been specified by the client.
+ if (mKeysToSync.isEmpty()) {
+ return;
+ }
+
+ bulkSyncData();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void bulkSyncData() {
+ // Collect data in a bundle
+ final Bundle data = new Bundle();
+ final SharedPreferences pref = getDefaultSharedPreferences();
+ final Map<String, ?> allData = pref.getAll();
+ final ArrayList<SharedPreferencesKey> keysWithTypeBeingSynced = new ArrayList<>();
+
+ for (int i = 0; i < mKeysToSync.size(); i++) {
+ final String key = mKeysToSync.valueAt(i);
+ final Object value = allData.get(key);
+ if (value == null) {
+ // Keep the key missing from the bundle; that means key has been removed.
+ // Type of missing key doesn't matter, so we use a random type.
+ keysWithTypeBeingSynced.add(
+ new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING));
+ continue;
+ }
+ final SharedPreferencesKey keyWithTypeAdded = updateBundle(data, key, value);
+ keysWithTypeBeingSynced.add(keyWithTypeAdded);
+ }
+
+ final SharedPreferencesUpdate update =
+ new SharedPreferencesUpdate(keysWithTypeBeingSynced, data);
+ try {
+ mService.syncDataFromClient(
+ mContext.getPackageName(),
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis(),
+ update,
+ mCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage());
+ }
+ }
+
+ private SharedPreferences getDefaultSharedPreferences() {
+ final Context appContext = mContext.getApplicationContext();
+ return PreferenceManager.getDefaultSharedPreferences(appContext);
+ }
+
+ private class ChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences pref, @Nullable String key) {
+ // Sync specified keys only
+ synchronized (mLock) {
+ // Do not sync if we are in waiting state
+ if (mWaitingForSandbox) {
+ return;
+ }
+
+ if (key == null) {
+ // All keys have been cleared. Bulk sync so that we send null for every key.
+ bulkSyncData();
+ return;
+ }
+
+ if (!mKeysToSync.contains(key)) {
+ return;
+ }
+
+ final Bundle data = new Bundle();
+ SharedPreferencesKey keyWithType;
+ final Object value = pref.getAll().get(key);
+ if (value != null) {
+ keyWithType = updateBundle(data, key, value);
+ } else {
+ keyWithType =
+ new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING);
+ }
+
+ final SharedPreferencesUpdate update =
+ new SharedPreferencesUpdate(List.of(keyWithType), data);
+ try {
+ mService.syncDataFromClient(
+ mContext.getPackageName(),
+ /*timeAppCalledSystemServer=*/ System.currentTimeMillis(),
+ update,
+ mCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds key to bundle based on type of value
+ *
+ * @return SharedPreferenceKey of the key that has been added
+ */
+ @GuardedBy("mLock")
+ private SharedPreferencesKey updateBundle(Bundle data, String key, Object value) {
+ final String type = value.getClass().getSimpleName();
+ try {
+ switch (type) {
+ case "String":
+ data.putString(key, value.toString());
+ return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING);
+ case "Boolean":
+ data.putBoolean(key, (Boolean) value);
+ return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_BOOLEAN);
+ case "Integer":
+ data.putInt(key, (Integer) value);
+ return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_INTEGER);
+ case "Float":
+ data.putFloat(key, (Float) value);
+ return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_FLOAT);
+ case "Long":
+ data.putLong(key, (Long) value);
+ return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_LONG);
+ case "HashSet":
+ // TODO(b/239403323): Verify the set contains string
+ data.putStringArrayList(key, new ArrayList<>((Set<String>) value));
+ return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING_SET);
+ default:
+ Log.e(
+ TAG,
+ "Unknown type found in default SharedPreferences for Key: "
+ + key
+ + " type: "
+ + type);
+ }
+ } catch (ClassCastException ignore) {
+ data.remove(key);
+ Log.e(
+ TAG,
+ "Wrong type found in default SharedPreferences for Key: "
+ + key
+ + " Type: "
+ + type);
+ }
+ // By default, assume it's string
+ return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING);
+ }
+
+ private class SharedPreferencesSyncCallback extends ISharedPreferencesSyncCallback.Stub {
+ @Override
+ public void onSandboxStart() {
+ synchronized (mLock) {
+ if (mWaitingForSandbox) {
+ // Retry bulk sync if we were waiting for sandbox to start
+ mWaitingForSandbox = false;
+ bulkSyncData();
+ }
+ }
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMsg) {
+ synchronized (mLock) {
+ // Transition to waiting state when sandbox is unavailable
+ if (!mWaitingForSandbox
+ && errorCode == ISharedPreferencesSyncCallback.SANDBOX_NOT_AVAILABLE) {
+ Log.w(TAG, "Waiting for SdkSandbox: " + errorMsg);
+ // Wait for sandbox to start. When it starts, server will call onSandboxStart
+ mWaitingForSandbox = true;
+ return;
+ }
+ Log.e(TAG, "errorCode: " + errorCode + " errorMsg: " + errorMsg);
+ }
+ }
+ }
+}
diff --git a/android-34/android/app/sdksandbox/SharedPreferencesUpdate.java b/android-34/android/app/sdksandbox/SharedPreferencesUpdate.java
new file mode 100644
index 0000000..05e86be
--- /dev/null
+++ b/android-34/android/app/sdksandbox/SharedPreferencesUpdate.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox;
+
+import android.annotation.NonNull;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+// TODO(b/239403323): Add unit tests for this class.
+/**
+ * Class to encapsulate change in {@link SharedPreferences}.
+ *
+ * <p>To be used for passing updates to Sandbox for syncing data via {@link
+ * SharedPreferencesSyncManager#syncData()}.
+ *
+ * <p>Each update instance contains a list of {@link SharedPreferencesKey}, which are the keys whose
+ * updates are being sent over with this class. User can get the list using {@link
+ * #getKeysInUpdate}.
+ *
+ * <p>The data associated with the keys are sent as a {@link Bundle} which can be retrieved using
+ * {@link #getData()}. If a key is present in list returned in {@link #getKeysInUpdate} but missing
+ * in the {@link Bundle}, then that key has been removed in the update.
+ *
+ * @hide
+ */
+public final class SharedPreferencesUpdate implements Parcelable {
+
+ private final ArrayList<SharedPreferencesKey> mKeysToSync;
+ private final Bundle mData;
+
+ public static final @NonNull Parcelable.Creator<SharedPreferencesUpdate> CREATOR =
+ new Parcelable.Creator<SharedPreferencesUpdate>() {
+ public SharedPreferencesUpdate createFromParcel(Parcel in) {
+ return new SharedPreferencesUpdate(in);
+ }
+
+ public SharedPreferencesUpdate[] newArray(int size) {
+ return new SharedPreferencesUpdate[size];
+ }
+ };
+
+ public SharedPreferencesUpdate(
+ @NonNull Collection<SharedPreferencesKey> keysToSync, @NonNull Bundle data) {
+ Objects.requireNonNull(keysToSync, "keysToSync should not be null");
+ Objects.requireNonNull(data, "data should not be null");
+
+ mKeysToSync = new ArrayList<>(keysToSync);
+ mData = new Bundle(data);
+ }
+
+ private SharedPreferencesUpdate(Parcel in) {
+ mKeysToSync =
+ in.readArrayList(
+ SharedPreferencesKey.class.getClassLoader(), SharedPreferencesKey.class);
+ Objects.requireNonNull(mKeysToSync, "mKeysToSync should not be null");
+
+ mData = Bundle.CREATOR.createFromParcel(in);
+ Objects.requireNonNull(mData, "mData should not be null");
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeList(mKeysToSync);
+ mData.writeToParcel(out, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public List<SharedPreferencesKey> getKeysInUpdate() {
+ return mKeysToSync;
+ }
+
+ @NonNull
+ public Bundle getData() {
+ return mData;
+ }
+}
diff --git a/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxActivityHandler.java b/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxActivityHandler.java
new file mode 100644
index 0000000..002ee11
--- /dev/null
+++ b/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxActivityHandler.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox.sdkprovider;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.os.Build;
+import android.os.IBinder;
+import android.view.View;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * This is used to notify the SDK when an {@link Activity} is created for it.
+ *
+ * <p>When an SDK wants to start an {@link Activity}, it should register an implementation of this
+ * class by calling {@link
+ * SdkSandboxController#registerSdkSandboxActivityHandler(SdkSandboxActivityHandler)} that will
+ * return an {@link android.os.IBinder} identifier for the registered {@link
+ * SdkSandboxActivityHandler} to The SDK.
+ *
+ * <p>The SDK should be notified about the {@link Activity} creation by calling {@link
+ * SdkSandboxActivityHandler#onActivityCreated(Activity)} which happens when the caller app calls
+ * {@link android.app.sdksandbox.SdkSandboxManager#startSdkSandboxActivity(Activity, IBinder)} using
+ * the same {@link IBinder} identifier for the registered {@link SdkSandboxActivityHandler}.
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public interface SdkSandboxActivityHandler {
+ /**
+ * Notifies SDK when an {@link Activity} gets created.
+ *
+ * <p>This function is called synchronously from the main thread of the {@link Activity} that is
+ * getting created.
+ *
+ * <p>SDK is expected to call {@link Activity#setContentView(View)} to the passed {@link
+ * Activity} object to populate the view.
+ *
+ * @param activity the {@link Activity} gets created
+ */
+ void onActivityCreated(@NonNull Activity activity);
+}
diff --git a/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxActivityRegistry.java b/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxActivityRegistry.java
new file mode 100644
index 0000000..cd5e9a6
--- /dev/null
+++ b/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxActivityRegistry.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.sdksandbox.sdkprovider;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.ArrayMap;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Map;
+
+/**
+ * It is a Singleton class to store the registered {@link SdkSandboxActivityHandler} instances and
+ * their associated {@link Activity} instances.
+ *
+ * @hide
+ */
+public class SdkSandboxActivityRegistry {
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static SdkSandboxActivityRegistry sInstance;
+
+ // A lock to keep all map synchronized
+ private final Object mMapsLock = new Object();
+
+ @GuardedBy("mMapsLock")
+ private final Map<SdkSandboxActivityHandler, HandlerInfo> mHandlerToHandlerInfoMap =
+ new ArrayMap<>();
+
+ @GuardedBy("mMapsLock")
+ private final Map<IBinder, HandlerInfo> mTokenToHandlerInfoMap = new ArrayMap<>();
+
+ private SdkSandboxActivityRegistry() {}
+
+ /** Returns a singleton instance of this class. */
+ public static SdkSandboxActivityRegistry getInstance() {
+ synchronized (sLock) {
+ if (sInstance == null) {
+ sInstance = new SdkSandboxActivityRegistry();
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Registers the passed {@link SdkSandboxActivityHandler} and returns a {@link IBinder} token
+ * that identifies it.
+ *
+ * <p>If {@link SdkSandboxActivityHandler} is already registered, its {@link IBinder} identifier
+ * will be returned.
+ *
+ * @param sdkName is the name of the SDK registering {@link SdkSandboxActivityHandler}
+ * @param handler is the {@link SdkSandboxActivityHandler} to register.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @NonNull
+ public IBinder register(@NonNull String sdkName, @NonNull SdkSandboxActivityHandler handler) {
+ synchronized (mMapsLock) {
+ if (mHandlerToHandlerInfoMap.containsKey(handler)) {
+ HandlerInfo handlerInfo = mHandlerToHandlerInfoMap.get(handler);
+ return handlerInfo.getToken();
+ }
+
+ IBinder token = new Binder();
+ HandlerInfo handlerInfo = new HandlerInfo(sdkName, handler, token);
+ mHandlerToHandlerInfoMap.put(handlerInfo.getHandler(), handlerInfo);
+ mTokenToHandlerInfoMap.put(handlerInfo.getToken(), handlerInfo);
+ return token;
+ }
+ }
+
+ /**
+ * Unregisters the passed {@link SdkSandboxActivityHandler}.
+ *
+ * @param handler is the {@link SdkSandboxActivityHandler} to unregister.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void unregister(@NonNull SdkSandboxActivityHandler handler) {
+ synchronized (mMapsLock) {
+ HandlerInfo handlerInfo = mHandlerToHandlerInfoMap.get(handler);
+ if (handlerInfo == null) {
+ return;
+ }
+ mHandlerToHandlerInfoMap.remove(handlerInfo.getHandler());
+ mTokenToHandlerInfoMap.remove(handlerInfo.getToken());
+ }
+ }
+
+ /**
+ * It notifies the SDK about {@link Activity} creation.
+ *
+ * <p>This should be called by the sandbox {@link Activity} while being created to notify the
+ * SDK that registered the {@link SdkSandboxActivityHandler} that identified by the passed
+ * {@link IBinder} token.
+ *
+ * @param token is the {@link IBinder} identifier for the {@link SdkSandboxActivityHandler}.
+ * @param activity is the {@link Activity} is being created.
+ * @throws IllegalArgumentException if there is no registered handler identified by the passed
+ * {@link IBinder} token (that mostly would mean that the handler is de-registered before
+ * the passed {@link Activity} is created), or the {@link SdkSandboxActivityHandler} is
+ * already notified about a previous {@link Activity}, in both cases the passed {@link
+ * Activity} will not start.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void notifyOnActivityCreation(@NonNull IBinder token, @NonNull Activity activity) {
+ synchronized (mMapsLock) {
+ HandlerInfo handlerInfo = mTokenToHandlerInfoMap.get(token);
+ if (handlerInfo == null) {
+ throw new IllegalArgumentException(
+ "There is no registered SdkSandboxActivityHandler to notify");
+ }
+ handlerInfo.getHandler().onActivityCreated(activity);
+ }
+ }
+
+ /**
+ * Holds the information about {@link SdkSandboxActivityHandler}.
+ *
+ * @hide
+ */
+ private static class HandlerInfo {
+ private final String mSdkName;
+ private final SdkSandboxActivityHandler mHandler;
+ private final IBinder mToken;
+
+
+ HandlerInfo(String sdkName, SdkSandboxActivityHandler handler, IBinder token) {
+ this.mSdkName = sdkName;
+ this.mHandler = handler;
+ this.mToken = token;
+ }
+
+ @NonNull
+ public String getSdkName() {
+ return mSdkName;
+ }
+
+ @NonNull
+ public SdkSandboxActivityHandler getHandler() {
+ return mHandler;
+ }
+
+ @NonNull
+ public IBinder getToken() {
+ return mToken;
+ }
+ }
+}
diff --git a/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxController.java b/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxController.java
new file mode 100644
index 0000000..c3f5174
--- /dev/null
+++ b/android-34/android/app/sdksandbox/sdkprovider/SdkSandboxController.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.sdksandbox.sdkprovider;
+
+import static android.app.sdksandbox.sdkprovider.SdkSandboxController.SDK_SANDBOX_CONTROLLER_SERVICE;
+
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.app.Activity;
+import android.app.sdksandbox.SandboxedSdk;
+import android.app.sdksandbox.SandboxedSdkContext;
+import android.app.sdksandbox.SandboxedSdkProvider;
+import android.app.sdksandbox.SdkSandboxLocalSingleton;
+import android.app.sdksandbox.SdkSandboxManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.modules.utils.build.SdkLevel;
+
+import java.util.List;
+
+/**
+ * Controller that is used by SDK loaded in the sandbox to access information provided by the sdk
+ * sandbox.
+ *
+ * <p>It enables the SDK to communicate with other SDKS in the SDK sandbox and know about the state
+ * of the sdks that are currently loaded in it.
+ *
+ * <p>An instance of {@link SdkSandboxController} can be obtained using {@link
+ * Context#getSystemService} and {@link SdkSandboxController class}. The {@link Context} can in turn
+ * be obtained using {@link android.app.sdksandbox.SandboxedSdkProvider#getContext()}.
+ */
+@SystemService(SDK_SANDBOX_CONTROLLER_SERVICE)
+public class SdkSandboxController {
+ public static final String SDK_SANDBOX_CONTROLLER_SERVICE = "sdk_sandbox_controller_service";
+ /** @hide */
+ public static final String CLIENT_SHARED_PREFERENCES_NAME =
+ "com.android.sdksandbox.client_sharedpreferences";
+
+ private static final String TAG = "SdkSandboxController";
+
+ private SdkSandboxLocalSingleton mSdkSandboxLocalSingleton;
+ private SdkSandboxActivityRegistry mSdkSandboxActivityRegistry;
+ private Context mContext;
+
+ /**
+ * Create SdkSandboxController.
+ *
+ * @hide
+ */
+ public SdkSandboxController(@NonNull Context context) {
+ // When SdkSandboxController is initiated from inside the sdk sandbox process, its private
+ // members will be immediately rewritten by the initialize method.
+ initialize(context);
+ }
+
+ /**
+ * Initializes {@link SdkSandboxController} with the given {@code context}.
+ *
+ * <p>This method is called by the {@link SandboxedSdkContext} to propagate the correct context.
+ * For more information check the javadoc on the {@link
+ * android.app.sdksandbox.SdkSandboxSystemServiceRegistry}.
+ *
+ * @hide
+ * @see android.app.sdksandbox.SdkSandboxSystemServiceRegistry
+ */
+ public SdkSandboxController initialize(@NonNull Context context) {
+ mContext = context;
+ mSdkSandboxLocalSingleton = SdkSandboxLocalSingleton.getExistingInstance();
+ mSdkSandboxActivityRegistry = SdkSandboxActivityRegistry.getInstance();
+ return this;
+ }
+
+ /**
+ * Fetches information about Sdks that are loaded in the sandbox.
+ *
+ * @return List of {@link SandboxedSdk} containing all currently loaded sdks
+ * @throws UnsupportedOperationException if the controller is obtained from an unexpected
+ * context. Use {@link SandboxedSdkProvider#getContext()} for the right context
+ */
+ public @NonNull List<SandboxedSdk> getSandboxedSdks() {
+ enforceSandboxedSdkContextInitialization();
+ try {
+ return mSdkSandboxLocalSingleton
+ .getSdkToServiceCallback()
+ .getSandboxedSdks(((SandboxedSdkContext) mContext).getClientPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns {@link SharedPreferences} containing data synced from the client app.
+ *
+ * <p>Keys that have been synced by the client app using {@link
+ * SdkSandboxManager#addSyncedSharedPreferencesKeys(Set)} can be found in this {@link
+ * SharedPreferences}.
+ *
+ * <p>The returned {@link SharedPreferences} should only be read. Writing to it is not
+ * supported.
+ *
+ * @return {@link SharedPreferences} containing data synced from client app.
+ * @throws UnsupportedOperationException if the controller is obtained from an unexpected
+ * context. Use {@link SandboxedSdkProvider#getContext()} for the right context
+ */
+ @NonNull
+ public SharedPreferences getClientSharedPreferences() {
+ enforceSandboxedSdkContextInitialization();
+
+ // TODO(b/248214708): We should store synced data in a separate internal storage directory.
+ return mContext.getApplicationContext()
+ .getSharedPreferences(CLIENT_SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * Returns an identifier for a {@link SdkSandboxActivityHandler} after registering it.
+ *
+ * <p>This function registers an implementation of {@link SdkSandboxActivityHandler} created by
+ * an SDK and returns an {@link IBinder} which uniquely identifies the passed {@link
+ * SdkSandboxActivityHandler} object.
+ *
+ * <p>If the same {@link SdkSandboxActivityHandler} registered multiple times without
+ * unregistering, the same {@link IBinder} token will be returned.
+ *
+ * @param sdkSandboxActivityHandler is the {@link SdkSandboxActivityHandler} to register.
+ * @return {@link IBinder} uniquely identify the passed {@link SdkSandboxActivityHandler}.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @NonNull
+ public IBinder registerSdkSandboxActivityHandler(
+ @NonNull SdkSandboxActivityHandler sdkSandboxActivityHandler) {
+ if (!SdkLevel.isAtLeastU()) {
+ throw new UnsupportedOperationException();
+ }
+ enforceSandboxedSdkContextInitialization();
+
+ return mSdkSandboxActivityRegistry.register(getSdkName(), sdkSandboxActivityHandler);
+ }
+
+ /**
+ * Unregister an already registered {@link SdkSandboxActivityHandler}.
+ *
+ * <p>If the passed {@link SdkSandboxActivityHandler} is registered, it will be unregistered.
+ * Otherwise, it will do nothing.
+ *
+ * <p>After unregistering, SDK can register the same handler object again or create a new one in
+ * case it wants a new {@link Activity}.
+ *
+ * <p>If the {@link IBinder} token of the unregistered handler used to start a {@link Activity},
+ * the {@link Activity} will fail to start.
+ *
+ * @param sdkSandboxActivityHandler is the {@link SdkSandboxActivityHandler} to unregister.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @NonNull
+ public void unregisterSdkSandboxActivityHandler(
+ @NonNull SdkSandboxActivityHandler sdkSandboxActivityHandler) {
+ if (!SdkLevel.isAtLeastU()) {
+ throw new UnsupportedOperationException();
+ }
+ enforceSandboxedSdkContextInitialization();
+
+ mSdkSandboxActivityRegistry.unregister(sdkSandboxActivityHandler);
+ }
+
+ private void enforceSandboxedSdkContextInitialization() {
+ if (!(mContext instanceof SandboxedSdkContext)) {
+ throw new UnsupportedOperationException(
+ "Only available from the context obtained by calling android.app.sdksandbox"
+ + ".SandboxedSdkProvider#getContext()");
+ }
+ }
+
+ @NonNull
+ private String getSdkName() {
+ return ((SandboxedSdkContext) mContext).getSdkName();
+ }
+}
diff --git a/android-34/android/app/usage/NetworkStats.java b/android-34/android/app/usage/NetworkStats.java
new file mode 100644
index 0000000..26841de
--- /dev/null
+++ b/android-34/android/app/usage/NetworkStats.java
@@ -0,0 +1,744 @@
+/**
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.usage;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.INetworkStatsService;
+import android.net.INetworkStatsSession;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TrafficStats;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.net.module.util.CollectionUtils;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+
+/**
+ * Class providing enumeration over buckets of network usage statistics. {@link NetworkStats}
+ * objects are returned as results to various queries in {@link NetworkStatsManager}.
+ */
+public final class NetworkStats implements AutoCloseable {
+ private static final String TAG = "NetworkStats";
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ /**
+ * Start timestamp of stats collected
+ */
+ private final long mStartTimeStamp;
+
+ /**
+ * End timestamp of stats collected
+ */
+ private final long mEndTimeStamp;
+
+ /**
+ * Non-null array indicates the query enumerates over uids.
+ */
+ private int[] mUids;
+
+ /**
+ * Index of the current uid in mUids when doing uid enumeration or a single uid value,
+ * depending on query type.
+ */
+ private int mUidOrUidIndex;
+
+ /**
+ * Tag id in case if was specified in the query.
+ */
+ private int mTag = android.net.NetworkStats.TAG_NONE;
+
+ /**
+ * State in case it was not specified in the query.
+ */
+ private int mState = Bucket.STATE_ALL;
+
+ /**
+ * The session while the query requires it, null if all the stats have been collected or close()
+ * has been called.
+ */
+ private INetworkStatsSession mSession;
+ private NetworkTemplate mTemplate;
+
+ /**
+ * Results of a summary query.
+ */
+ private android.net.NetworkStats mSummary = null;
+
+ /**
+ * Results of detail queries.
+ */
+ private NetworkStatsHistory mHistory = null;
+
+ /**
+ * Where we are in enumerating over the current result.
+ */
+ private int mEnumerationIndex = 0;
+
+ /**
+ * Recycling entry objects to prevent heap fragmentation.
+ */
+ private android.net.NetworkStats.Entry mRecycledSummaryEntry = null;
+ private NetworkStatsHistory.Entry mRecycledHistoryEntry = null;
+
+ /** @hide */
+ NetworkStats(Context context, NetworkTemplate template, int flags, long startTimestamp,
+ long endTimestamp, INetworkStatsService statsService)
+ throws RemoteException, SecurityException {
+ // Open network stats session
+ mSession = statsService.openSessionForUsageStats(flags, context.getOpPackageName());
+ mCloseGuard.open("close");
+ mTemplate = template;
+ mStartTimeStamp = startTimestamp;
+ mEndTimeStamp = endTimestamp;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ // -------------------------BEGINNING OF PUBLIC API-----------------------------------
+
+ /**
+ * Buckets are the smallest elements of a query result. As some dimensions of a result may be
+ * aggregated (e.g. time or state) some values may be equal across all buckets.
+ */
+ public static class Bucket {
+ /** @hide */
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_ALL,
+ STATE_DEFAULT,
+ STATE_FOREGROUND
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface State {}
+
+ /**
+ * Combined usage across all states.
+ */
+ public static final int STATE_ALL = -1;
+
+ /**
+ * Usage not accounted for in any other state.
+ */
+ public static final int STATE_DEFAULT = 0x1;
+
+ /**
+ * Foreground usage.
+ */
+ public static final int STATE_FOREGROUND = 0x2;
+
+ /**
+ * Special UID value for aggregate/unspecified.
+ */
+ public static final int UID_ALL = android.net.NetworkStats.UID_ALL;
+
+ /**
+ * Special UID value for removed apps.
+ */
+ public static final int UID_REMOVED = TrafficStats.UID_REMOVED;
+
+ /**
+ * Special UID value for data usage by tethering.
+ */
+ public static final int UID_TETHERING = TrafficStats.UID_TETHERING;
+
+ /** @hide */
+ @IntDef(prefix = { "METERED_" }, value = {
+ METERED_ALL,
+ METERED_NO,
+ METERED_YES
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Metered {}
+
+ /**
+ * Combined usage across all metered states. Covers metered and unmetered usage.
+ */
+ public static final int METERED_ALL = -1;
+
+ /**
+ * Usage that occurs on an unmetered network.
+ */
+ public static final int METERED_NO = 0x1;
+
+ /**
+ * Usage that occurs on a metered network.
+ *
+ * <p>A network is classified as metered when the user is sensitive to heavy data usage on
+ * that connection.
+ */
+ public static final int METERED_YES = 0x2;
+
+ /** @hide */
+ @IntDef(prefix = { "ROAMING_" }, value = {
+ ROAMING_ALL,
+ ROAMING_NO,
+ ROAMING_YES
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Roaming {}
+
+ /**
+ * Combined usage across all roaming states. Covers both roaming and non-roaming usage.
+ */
+ public static final int ROAMING_ALL = -1;
+
+ /**
+ * Usage that occurs on a home, non-roaming network.
+ *
+ * <p>Any cellular usage in this bucket was incurred while the device was connected to a
+ * tower owned or operated by the user's wireless carrier, or a tower that the user's
+ * wireless carrier has indicated should be treated as a home network regardless.
+ *
+ * <p>This is also the default value for network types that do not support roaming.
+ */
+ public static final int ROAMING_NO = 0x1;
+
+ /**
+ * Usage that occurs on a roaming network.
+ *
+ * <p>Any cellular usage in this bucket as incurred while the device was roaming on another
+ * carrier's network, for which additional charges may apply.
+ */
+ public static final int ROAMING_YES = 0x2;
+
+ /** @hide */
+ @IntDef(prefix = { "DEFAULT_NETWORK_" }, value = {
+ DEFAULT_NETWORK_ALL,
+ DEFAULT_NETWORK_NO,
+ DEFAULT_NETWORK_YES
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DefaultNetworkStatus {}
+
+ /**
+ * Combined usage for this network regardless of default network status.
+ */
+ public static final int DEFAULT_NETWORK_ALL = -1;
+
+ /**
+ * Usage that occurs while this network is not a default network.
+ *
+ * <p>This implies that the app responsible for this usage requested that it occur on a
+ * specific network different from the one(s) the system would have selected for it.
+ */
+ public static final int DEFAULT_NETWORK_NO = 0x1;
+
+ /**
+ * Usage that occurs while this network is a default network.
+ *
+ * <p>This implies that the app either did not select a specific network for this usage,
+ * or it selected a network that the system could have selected for app traffic.
+ */
+ public static final int DEFAULT_NETWORK_YES = 0x2;
+
+ /**
+ * Special TAG value for total data across all tags
+ */
+ public static final int TAG_NONE = android.net.NetworkStats.TAG_NONE;
+
+ private int mUid;
+ private int mTag;
+ private int mState;
+ private int mDefaultNetworkStatus;
+ private int mMetered;
+ private int mRoaming;
+ private long mBeginTimeStamp;
+ private long mEndTimeStamp;
+ private long mRxBytes;
+ private long mRxPackets;
+ private long mTxBytes;
+ private long mTxPackets;
+
+ private static int convertSet(@State int state) {
+ switch (state) {
+ case STATE_ALL: return android.net.NetworkStats.SET_ALL;
+ case STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
+ case STATE_FOREGROUND: return android.net.NetworkStats.SET_FOREGROUND;
+ }
+ return 0;
+ }
+
+ private static @State int convertState(int networkStatsSet) {
+ switch (networkStatsSet) {
+ case android.net.NetworkStats.SET_ALL : return STATE_ALL;
+ case android.net.NetworkStats.SET_DEFAULT : return STATE_DEFAULT;
+ case android.net.NetworkStats.SET_FOREGROUND : return STATE_FOREGROUND;
+ }
+ return 0;
+ }
+
+ private static int convertUid(int uid) {
+ switch (uid) {
+ case TrafficStats.UID_REMOVED: return UID_REMOVED;
+ case TrafficStats.UID_TETHERING: return UID_TETHERING;
+ }
+ return uid;
+ }
+
+ private static int convertTag(int tag) {
+ switch (tag) {
+ case android.net.NetworkStats.TAG_NONE: return TAG_NONE;
+ }
+ return tag;
+ }
+
+ private static @Metered int convertMetered(int metered) {
+ switch (metered) {
+ case android.net.NetworkStats.METERED_ALL : return METERED_ALL;
+ case android.net.NetworkStats.METERED_NO: return METERED_NO;
+ case android.net.NetworkStats.METERED_YES: return METERED_YES;
+ }
+ return 0;
+ }
+
+ private static @Roaming int convertRoaming(int roaming) {
+ switch (roaming) {
+ case android.net.NetworkStats.ROAMING_ALL : return ROAMING_ALL;
+ case android.net.NetworkStats.ROAMING_NO: return ROAMING_NO;
+ case android.net.NetworkStats.ROAMING_YES: return ROAMING_YES;
+ }
+ return 0;
+ }
+
+ private static @DefaultNetworkStatus int convertDefaultNetworkStatus(
+ int defaultNetworkStatus) {
+ switch (defaultNetworkStatus) {
+ case android.net.NetworkStats.DEFAULT_NETWORK_ALL : return DEFAULT_NETWORK_ALL;
+ case android.net.NetworkStats.DEFAULT_NETWORK_NO: return DEFAULT_NETWORK_NO;
+ case android.net.NetworkStats.DEFAULT_NETWORK_YES: return DEFAULT_NETWORK_YES;
+ }
+ return 0;
+ }
+
+ public Bucket() {
+ }
+
+ /**
+ * Key of the bucket. Usually an app uid or one of the following special values:<p />
+ * <ul>
+ * <li>{@link #UID_REMOVED}</li>
+ * <li>{@link #UID_TETHERING}</li>
+ * <li>{@link android.os.Process#SYSTEM_UID}</li>
+ * </ul>
+ * @return Bucket key.
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Tag of the bucket.<p />
+ * @return Bucket tag.
+ */
+ public int getTag() {
+ return mTag;
+ }
+
+ /**
+ * Usage state. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #STATE_ALL}</li>
+ * <li>{@link #STATE_DEFAULT}</li>
+ * <li>{@link #STATE_FOREGROUND}</li>
+ * </ul>
+ * @return Usage state.
+ */
+ public @State int getState() {
+ return mState;
+ }
+
+ /**
+ * Metered state. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #METERED_ALL}</li>
+ * <li>{@link #METERED_NO}</li>
+ * <li>{@link #METERED_YES}</li>
+ * </ul>
+ * <p>A network is classified as metered when the user is sensitive to heavy data usage on
+ * that connection. Apps may warn before using these networks for large downloads. The
+ * metered state can be set by the user within data usage network restrictions.
+ */
+ public @Metered int getMetered() {
+ return mMetered;
+ }
+
+ /**
+ * Roaming state. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #ROAMING_ALL}</li>
+ * <li>{@link #ROAMING_NO}</li>
+ * <li>{@link #ROAMING_YES}</li>
+ * </ul>
+ */
+ public @Roaming int getRoaming() {
+ return mRoaming;
+ }
+
+ /**
+ * Default network status. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #DEFAULT_NETWORK_ALL}</li>
+ * <li>{@link #DEFAULT_NETWORK_NO}</li>
+ * <li>{@link #DEFAULT_NETWORK_YES}</li>
+ * </ul>
+ */
+ public @DefaultNetworkStatus int getDefaultNetworkStatus() {
+ return mDefaultNetworkStatus;
+ }
+
+ /**
+ * Start timestamp of the bucket's time interval. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Start of interval.
+ */
+ public long getStartTimeStamp() {
+ return mBeginTimeStamp;
+ }
+
+ /**
+ * End timestamp of the bucket's time interval. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return End of interval.
+ */
+ public long getEndTimeStamp() {
+ return mEndTimeStamp;
+ }
+
+ /**
+ * Number of bytes received during the bucket's time interval. Statistics are measured at
+ * the network layer, so they include both TCP and UDP usage.
+ * @return Number of bytes.
+ */
+ public long getRxBytes() {
+ return mRxBytes;
+ }
+
+ /**
+ * Number of bytes transmitted during the bucket's time interval. Statistics are measured at
+ * the network layer, so they include both TCP and UDP usage.
+ * @return Number of bytes.
+ */
+ public long getTxBytes() {
+ return mTxBytes;
+ }
+
+ /**
+ * Number of packets received during the bucket's time interval. Statistics are measured at
+ * the network layer, so they include both TCP and UDP usage.
+ * @return Number of packets.
+ */
+ public long getRxPackets() {
+ return mRxPackets;
+ }
+
+ /**
+ * Number of packets transmitted during the bucket's time interval. Statistics are measured
+ * at the network layer, so they include both TCP and UDP usage.
+ * @return Number of packets.
+ */
+ public long getTxPackets() {
+ return mTxPackets;
+ }
+ }
+
+ /**
+ * Fills the recycled bucket with data of the next bin in the enumeration.
+ * @param bucketOut Bucket to be filled with data. If null, the method does
+ * nothing and returning false.
+ * @return true if successfully filled the bucket, false otherwise.
+ */
+ public boolean getNextBucket(@Nullable Bucket bucketOut) {
+ if (mSummary != null) {
+ return getNextSummaryBucket(bucketOut);
+ } else {
+ return getNextHistoryBucket(bucketOut);
+ }
+ }
+
+ /**
+ * Check if it is possible to ask for a next bucket in the enumeration.
+ * @return true if there is at least one more bucket.
+ */
+ public boolean hasNextBucket() {
+ if (mSummary != null) {
+ return mEnumerationIndex < mSummary.size();
+ } else if (mHistory != null) {
+ return mEnumerationIndex < mHistory.size()
+ || hasNextUid();
+ }
+ return false;
+ }
+
+ /**
+ * Closes the enumeration. Call this method before this object gets out of scope.
+ */
+ @Override
+ public void close() {
+ if (mSession != null) {
+ try {
+ mSession.close();
+ } catch (RemoteException e) {
+ Log.w(TAG, e);
+ // Otherwise, meh
+ }
+ }
+ mSession = null;
+ if (mCloseGuard != null) {
+ mCloseGuard.close();
+ }
+ }
+
+ // -------------------------END OF PUBLIC API-----------------------------------
+
+ /**
+ * Collects device summary results into a Bucket.
+ * @throws RemoteException
+ */
+ Bucket getDeviceSummaryForNetwork() throws RemoteException {
+ mSummary = mSession.getDeviceSummaryForNetwork(mTemplate, mStartTimeStamp, mEndTimeStamp);
+
+ // Setting enumeration index beyond end to avoid accidental enumeration over data that does
+ // not belong to the calling user.
+ mEnumerationIndex = mSummary.size();
+
+ return getSummaryAggregate();
+ }
+
+ /**
+ * Collects summary results and sets summary enumeration mode.
+ * @throws RemoteException
+ */
+ void startSummaryEnumeration() throws RemoteException {
+ mSummary = mSession.getSummaryForAllUid(mTemplate, mStartTimeStamp, mEndTimeStamp,
+ false /* includeTags */);
+ mEnumerationIndex = 0;
+ }
+
+ /**
+ * Collects tagged summary results and sets summary enumeration mode.
+ * @throws RemoteException
+ */
+ void startTaggedSummaryEnumeration() throws RemoteException {
+ mSummary = mSession.getTaggedSummaryForAllUid(mTemplate, mStartTimeStamp, mEndTimeStamp);
+ mEnumerationIndex = 0;
+ }
+
+ /**
+ * Collects history results for uid and resets history enumeration index.
+ */
+ void startHistoryUidEnumeration(int uid, int tag, int state) {
+ mHistory = null;
+ try {
+ mHistory = mSession.getHistoryIntervalForUid(mTemplate, uid,
+ Bucket.convertSet(state), tag, NetworkStatsHistory.FIELD_ALL,
+ mStartTimeStamp, mEndTimeStamp);
+ setSingleUidTagState(uid, tag, state);
+ } catch (RemoteException e) {
+ Log.w(TAG, e);
+ // Leaving mHistory null
+ }
+ mEnumerationIndex = 0;
+ }
+
+ /**
+ * Collects history results for network and resets history enumeration index.
+ */
+ void startHistoryDeviceEnumeration() {
+ try {
+ mHistory = mSession.getHistoryIntervalForNetwork(
+ mTemplate, NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+ } catch (RemoteException e) {
+ Log.w(TAG, e);
+ mHistory = null;
+ }
+ mEnumerationIndex = 0;
+ }
+
+ /**
+ * Starts uid enumeration for current user.
+ * @throws RemoteException
+ */
+ void startUserUidEnumeration() throws RemoteException {
+ // TODO: getRelevantUids should be sensitive to time interval. When that's done,
+ // the filtering logic below can be removed.
+ int[] uids = mSession.getRelevantUids();
+ // Filtering of uids with empty history.
+ final ArrayList<Integer> filteredUids = new ArrayList<>();
+ for (int uid : uids) {
+ try {
+ NetworkStatsHistory history = mSession.getHistoryIntervalForUid(mTemplate, uid,
+ android.net.NetworkStats.SET_ALL, android.net.NetworkStats.TAG_NONE,
+ NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+ if (history != null && history.size() > 0) {
+ filteredUids.add(uid);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error while getting history of uid " + uid, e);
+ }
+ }
+ mUids = CollectionUtils.toIntArray(filteredUids);
+ mUidOrUidIndex = -1;
+ stepHistory();
+ }
+
+ /**
+ * Steps to next uid in enumeration and collects history for that.
+ */
+ private void stepHistory() {
+ if (hasNextUid()) {
+ stepUid();
+ mHistory = null;
+ try {
+ mHistory = mSession.getHistoryIntervalForUid(mTemplate, getUid(),
+ android.net.NetworkStats.SET_ALL, android.net.NetworkStats.TAG_NONE,
+ NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+ } catch (RemoteException e) {
+ Log.w(TAG, e);
+ // Leaving mHistory null
+ }
+ mEnumerationIndex = 0;
+ }
+ }
+
+ private void fillBucketFromSummaryEntry(Bucket bucketOut) {
+ bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid);
+ bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag);
+ bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set);
+ bucketOut.mDefaultNetworkStatus = Bucket.convertDefaultNetworkStatus(
+ mRecycledSummaryEntry.defaultNetwork);
+ bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered);
+ bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming);
+ bucketOut.mBeginTimeStamp = mStartTimeStamp;
+ bucketOut.mEndTimeStamp = mEndTimeStamp;
+ bucketOut.mRxBytes = mRecycledSummaryEntry.rxBytes;
+ bucketOut.mRxPackets = mRecycledSummaryEntry.rxPackets;
+ bucketOut.mTxBytes = mRecycledSummaryEntry.txBytes;
+ bucketOut.mTxPackets = mRecycledSummaryEntry.txPackets;
+ }
+
+ /**
+ * Getting the next item in summary enumeration.
+ * @param bucketOut Next item will be set here.
+ * @return true if a next item could be set.
+ */
+ private boolean getNextSummaryBucket(@Nullable Bucket bucketOut) {
+ if (bucketOut != null && mEnumerationIndex < mSummary.size()) {
+ mRecycledSummaryEntry = mSummary.getValues(mEnumerationIndex++, mRecycledSummaryEntry);
+ fillBucketFromSummaryEntry(bucketOut);
+ return true;
+ }
+ return false;
+ }
+
+ Bucket getSummaryAggregate() {
+ if (mSummary == null) {
+ return null;
+ }
+ Bucket bucket = new Bucket();
+ if (mRecycledSummaryEntry == null) {
+ mRecycledSummaryEntry = new android.net.NetworkStats.Entry();
+ }
+ mSummary.getTotal(mRecycledSummaryEntry);
+ fillBucketFromSummaryEntry(bucket);
+ return bucket;
+ }
+
+ /**
+ * Getting the next item in a history enumeration.
+ * @param bucketOut Next item will be set here.
+ * @return true if a next item could be set.
+ */
+ private boolean getNextHistoryBucket(@Nullable Bucket bucketOut) {
+ if (bucketOut != null && mHistory != null) {
+ if (mEnumerationIndex < mHistory.size()) {
+ mRecycledHistoryEntry = mHistory.getValues(mEnumerationIndex++,
+ mRecycledHistoryEntry);
+ bucketOut.mUid = Bucket.convertUid(getUid());
+ bucketOut.mTag = Bucket.convertTag(mTag);
+ bucketOut.mState = mState;
+ bucketOut.mDefaultNetworkStatus = Bucket.DEFAULT_NETWORK_ALL;
+ bucketOut.mMetered = Bucket.METERED_ALL;
+ bucketOut.mRoaming = Bucket.ROAMING_ALL;
+ bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart;
+ bucketOut.mEndTimeStamp = mRecycledHistoryEntry.bucketStart
+ + mRecycledHistoryEntry.bucketDuration;
+ bucketOut.mRxBytes = mRecycledHistoryEntry.rxBytes;
+ bucketOut.mRxPackets = mRecycledHistoryEntry.rxPackets;
+ bucketOut.mTxBytes = mRecycledHistoryEntry.txBytes;
+ bucketOut.mTxPackets = mRecycledHistoryEntry.txPackets;
+ return true;
+ } else if (hasNextUid()) {
+ stepHistory();
+ return getNextHistoryBucket(bucketOut);
+ }
+ }
+ return false;
+ }
+
+ // ------------------ UID LOGIC------------------------
+
+ private boolean isUidEnumeration() {
+ return mUids != null;
+ }
+
+ private boolean hasNextUid() {
+ return isUidEnumeration() && (mUidOrUidIndex + 1) < mUids.length;
+ }
+
+ private int getUid() {
+ // Check if uid enumeration.
+ if (isUidEnumeration()) {
+ if (mUidOrUidIndex < 0 || mUidOrUidIndex >= mUids.length) {
+ throw new IndexOutOfBoundsException(
+ "Index=" + mUidOrUidIndex + " mUids.length=" + mUids.length);
+ }
+ return mUids[mUidOrUidIndex];
+ }
+ // Single uid mode.
+ return mUidOrUidIndex;
+ }
+
+ private void setSingleUidTagState(int uid, int tag, int state) {
+ mUidOrUidIndex = uid;
+ mTag = tag;
+ mState = state;
+ }
+
+ private void stepUid() {
+ if (mUids != null) {
+ ++mUidOrUidIndex;
+ }
+ }
+}
diff --git a/android-34/android/app/usage/NetworkStatsManager.java b/android-34/android/app/usage/NetworkStatsManager.java
new file mode 100644
index 0000000..d139544
--- /dev/null
+++ b/android-34/android/app/usage/NetworkStatsManager.java
@@ -0,0 +1,1245 @@
+/**
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.WorkerThread;
+import android.app.usage.NetworkStats.Bucket;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.DataUsageRequest;
+import android.net.INetworkStatsService;
+import android.net.Network;
+import android.net.NetworkStack;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkTemplate;
+import android.net.UnderlyingNetworkInfo;
+import android.net.netstats.IUsageCallback;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.net.netstats.provider.NetworkStatsProvider;
+import android.os.Build;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.NetworkIdentityUtils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Provides access to network usage history and statistics. Usage data is collected in
+ * discrete bins of time called 'Buckets'. See {@link NetworkStats.Bucket} for details.
+ * <p />
+ * Queries can define a time interval in the form of start and end timestamps (Long.MIN_VALUE and
+ * Long.MAX_VALUE can be used to simulate open ended intervals). By default, apps can only obtain
+ * data about themselves. See the below note for special cases in which apps can obtain data about
+ * other applications.
+ * <h3>
+ * Summary queries
+ * </h3>
+ * {@link #querySummaryForDevice} <p />
+ * {@link #querySummaryForUser} <p />
+ * {@link #querySummary} <p />
+ * These queries aggregate network usage across the whole interval. Therefore there will be only one
+ * bucket for a particular key, state, metered and roaming combination. In case of the user-wide
+ * and device-wide summaries a single bucket containing the totalised network usage is returned.
+ * <h3>
+ * History queries
+ * </h3>
+ * {@link #queryDetailsForUid} <p />
+ * {@link #queryDetails} <p />
+ * These queries do not aggregate over time but do aggregate over state, metered and roaming.
+ * Therefore there can be multiple buckets for a particular key. However, all Buckets will have
+ * {@code state} {@link NetworkStats.Bucket#STATE_ALL},
+ * {@code defaultNetwork} {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * {@code metered } {@link NetworkStats.Bucket#METERED_ALL},
+ * {@code roaming} {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p />
+ * <b>NOTE:</b> Calling {@link #querySummaryForDevice} or accessing stats for apps other than the
+ * calling app requires the permission {@link android.Manifest.permission#PACKAGE_USAGE_STATS},
+ * which is a system-level permission and will not be granted to third-party apps. However,
+ * declaring the permission implies intention to use the API and the user of the device can grant
+ * permission through the Settings application.
+ * <p />
+ * Profile owner apps are automatically granted permission to query data on the profile they manage
+ * (that is, for any query except {@link #querySummaryForDevice}). Device owner apps and carrier-
+ * privileged apps likewise get access to usage data for all users on the device.
+ * <p />
+ * In addition to tethering usage, usage by removed users and apps, and usage by the system
+ * is also included in the results for callers with one of these higher levels of access.
+ * <p />
+ * <b>NOTE:</b> Prior to API level {@value android.os.Build.VERSION_CODES#N}, all calls to these APIs required
+ * the above permission, even to access an app's own data usage, and carrier-privileged apps were
+ * not included.
+ */
+@SystemService(Context.NETWORK_STATS_SERVICE)
+public class NetworkStatsManager {
+ private static final String TAG = "NetworkStatsManager";
+ private static final boolean DBG = false;
+
+ /** @hide */
+ public static final int CALLBACK_LIMIT_REACHED = 0;
+ /** @hide */
+ public static final int CALLBACK_RELEASED = 1;
+
+ /**
+ * Minimum data usage threshold for registering usage callbacks.
+ *
+ * Requests registered with a threshold lower than this will only be triggered once this minimum
+ * is reached.
+ * @hide
+ */
+ public static final long MIN_THRESHOLD_BYTES = 2 * 1_048_576L; // 2MiB
+
+ private final Context mContext;
+ private final INetworkStatsService mService;
+
+ /**
+ * @deprecated Use {@link NetworkStatsDataMigrationUtils#PREFIX_XT}
+ * instead.
+ * @hide
+ */
+ @Deprecated
+ public static final String PREFIX_DEV = "dev";
+
+ /** @hide */
+ public static final int FLAG_POLL_ON_OPEN = 1 << 0;
+ /** @hide */
+ public static final int FLAG_POLL_FORCE = 1 << 1;
+ /** @hide */
+ public static final int FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN = 1 << 2;
+
+ /**
+ * Virtual RAT type to represent 5G NSA (Non Stand Alone) mode, where the primary cell is
+ * still LTE and network allocates a secondary 5G cell so telephony reports RAT = LTE along
+ * with NR state as connected. This is a concept added by NetworkStats on top of the telephony
+ * constants for backward compatibility of metrics so this should not be overlapped with any of
+ * the {@code TelephonyManager.NETWORK_TYPE_*} constants.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int NETWORK_TYPE_5G_NSA = -2;
+
+ private int mFlags;
+
+ /** @hide */
+ @VisibleForTesting
+ public NetworkStatsManager(Context context, INetworkStatsService service) {
+ mContext = context;
+ mService = service;
+ setPollOnOpen(true);
+ setAugmentWithSubscriptionPlan(true);
+ }
+
+ /** @hide */
+ public INetworkStatsService getBinder() {
+ return mService;
+ }
+
+ /**
+ * Set poll on open flag to indicate the poll is needed before service gets statistics
+ * result. This is default enabled. However, for any non-privileged caller, the poll might
+ * be omitted in case of rate limiting.
+ *
+ * @param pollOnOpen true if poll is needed.
+ * @hide
+ */
+ // The system will ignore any non-default values for non-privileged
+ // processes, so processes that don't hold the appropriate permissions
+ // can make no use of this API.
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_STACK})
+ public void setPollOnOpen(boolean pollOnOpen) {
+ if (pollOnOpen) {
+ mFlags |= FLAG_POLL_ON_OPEN;
+ } else {
+ mFlags &= ~FLAG_POLL_ON_OPEN;
+ }
+ }
+
+ /**
+ * Set poll force flag to indicate that calling any subsequent query method will force a stats
+ * poll.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @SystemApi(client = MODULE_LIBRARIES)
+ public void setPollForce(boolean pollForce) {
+ if (pollForce) {
+ mFlags |= FLAG_POLL_FORCE;
+ } else {
+ mFlags &= ~FLAG_POLL_FORCE;
+ }
+ }
+
+ /** @hide */
+ public void setAugmentWithSubscriptionPlan(boolean augmentWithSubscriptionPlan) {
+ if (augmentWithSubscriptionPlan) {
+ mFlags |= FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN;
+ } else {
+ mFlags &= ~FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN;
+ }
+ }
+
+ /**
+ * Query network usage statistics summaries.
+ *
+ * Result is summarised data usage for the whole
+ * device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and
+ * roaming. This means the bucket's start and end timestamp will be the same as the
+ * 'startTime' and 'endTime' arguments. State is going to be
+ * {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL},
+ * tag {@link NetworkStats.Bucket#TAG_NONE},
+ * default network {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered {@link NetworkStats.Bucket#METERED_ALL},
+ * and roaming {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param template Template used to match networks. See {@link NetworkTemplate}.
+ * @param startTime Start of period, in milliseconds since the Unix epoch, see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period, in milliseconds since the Unix epoch, see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Bucket Summarised data usage.
+ *
+ * @hide
+ */
+ @NonNull
+ @WorkerThread
+ @SystemApi(client = MODULE_LIBRARIES)
+ public Bucket querySummaryForDevice(@NonNull NetworkTemplate template,
+ long startTime, long endTime) {
+ Objects.requireNonNull(template);
+ try {
+ NetworkStats stats =
+ new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+ Bucket bucket = stats.getDeviceSummaryForNetwork();
+ stats.close();
+ return bucket;
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return null; // To make the compiler happy.
+ }
+
+ /**
+ * Query network usage statistics summaries. Result is summarised data usage for the whole
+ * device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and
+ * roaming. This means the bucket's start and end timestamp are going to be the same as the
+ * 'startTime' and 'endTime' parameters. State is going to be
+ * {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL},
+ * tag {@link NetworkStats.Bucket#TAG_NONE},
+ * default network {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered {@link NetworkStats.Bucket#METERED_ALL},
+ * and roaming {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Bucket object or null if permissions are insufficient or error happened during
+ * statistics collection.
+ */
+ @WorkerThread
+ public Bucket querySummaryForDevice(int networkType, @Nullable String subscriberId,
+ long startTime, long endTime) throws SecurityException, RemoteException {
+ NetworkTemplate template;
+ try {
+ template = createTemplate(networkType, subscriberId);
+ } catch (IllegalArgumentException e) {
+ if (DBG) Log.e(TAG, "Cannot create template", e);
+ return null;
+ }
+
+ return querySummaryForDevice(template, startTime, endTime);
+ }
+
+ /**
+ * Query network usage statistics summaries. Result is summarised data usage for all uids
+ * belonging to calling user. Result is a single Bucket aggregated over time, state and uid.
+ * This means the bucket's start and end timestamp are going to be the same as the 'startTime'
+ * and 'endTime' parameters. State is going to be {@link NetworkStats.Bucket#STATE_ALL},
+ * uid {@link NetworkStats.Bucket#UID_ALL}, tag {@link NetworkStats.Bucket#TAG_NONE},
+ * metered {@link NetworkStats.Bucket#METERED_ALL}, and roaming
+ * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Bucket object or null if permissions are insufficient or error happened during
+ * statistics collection.
+ */
+ @WorkerThread
+ public Bucket querySummaryForUser(int networkType, @Nullable String subscriberId,
+ long startTime, long endTime) throws SecurityException, RemoteException {
+ NetworkTemplate template;
+ try {
+ template = createTemplate(networkType, subscriberId);
+ } catch (IllegalArgumentException e) {
+ if (DBG) Log.e(TAG, "Cannot create template", e);
+ return null;
+ }
+
+ NetworkStats stats;
+ stats = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+ stats.startSummaryEnumeration();
+
+ stats.close();
+ return stats.getSummaryAggregate();
+ }
+
+ /**
+ * Query network usage statistics summaries. Result filtered to include only uids belonging to
+ * calling user. Result is aggregated over time, hence all buckets will have the same start and
+ * end timestamps. Not aggregated over state, uid, default network, metered, or roaming. This
+ * means buckets' start and end timestamps are going to be the same as the 'startTime' and
+ * 'endTime' parameters. State, uid, metered, and roaming are going to vary, and tag is going to
+ * be the same.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Statistics object or null if permissions are insufficient or error happened during
+ * statistics collection.
+ */
+ @WorkerThread
+ public NetworkStats querySummary(int networkType, @Nullable String subscriberId, long startTime,
+ long endTime) throws SecurityException, RemoteException {
+ NetworkTemplate template;
+ try {
+ template = createTemplate(networkType, subscriberId);
+ } catch (IllegalArgumentException e) {
+ if (DBG) Log.e(TAG, "Cannot create template", e);
+ return null;
+ }
+
+ return querySummary(template, startTime, endTime);
+ }
+
+ /**
+ * Query network usage statistics summaries.
+ *
+ * The results will only include traffic made by UIDs belonging to the calling user profile.
+ * The results are aggregated over time, so that all buckets will have the same start and
+ * end timestamps as the passed arguments. Not aggregated over state, uid, default network,
+ * metered, or roaming.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param template Template used to match networks. See {@link NetworkTemplate}.
+ * @param startTime Start of period, in milliseconds since the Unix epoch, see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period, in milliseconds since the Unix epoch, see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Statistics which is described above.
+ * @hide
+ */
+ @NonNull
+ @SystemApi(client = MODULE_LIBRARIES)
+ @WorkerThread
+ public NetworkStats querySummary(@NonNull NetworkTemplate template, long startTime,
+ long endTime) throws SecurityException {
+ Objects.requireNonNull(template);
+ try {
+ NetworkStats result =
+ new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+ result.startSummaryEnumeration();
+ return result;
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return null; // To make the compiler happy.
+ }
+
+ /**
+ * Query tagged network usage statistics summaries.
+ *
+ * The results will only include tagged traffic made by UIDs belonging to the calling user
+ * profile. The results are aggregated over time, so that all buckets will have the same
+ * start and end timestamps as the passed arguments. Not aggregated over state, uid,
+ * default network, metered, or roaming.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param template Template used to match networks. See {@link NetworkTemplate}.
+ * @param startTime Start of period, in milliseconds since the Unix epoch, see
+ * {@link System#currentTimeMillis}.
+ * @param endTime End of period, in milliseconds since the Unix epoch, see
+ * {@link System#currentTimeMillis}.
+ * @return Statistics which is described above.
+ * @hide
+ */
+ @NonNull
+ @SystemApi(client = MODULE_LIBRARIES)
+ @WorkerThread
+ public NetworkStats queryTaggedSummary(@NonNull NetworkTemplate template, long startTime,
+ long endTime) throws SecurityException {
+ Objects.requireNonNull(template);
+ try {
+ NetworkStats result =
+ new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+ result.startTaggedSummaryEnumeration();
+ return result;
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return null; // To make the compiler happy.
+ }
+
+ /**
+ * Query usage statistics details for networks matching a given {@link NetworkTemplate}.
+ *
+ * Result is not aggregated over time. This means buckets' start and
+ * end timestamps will be between 'startTime' and 'endTime' parameters.
+ * <p>Only includes buckets whose entire time period is included between
+ * startTime and endTime. Doesn't interpolate or return partial buckets.
+ * Since bucket length is in the order of hours, this
+ * method cannot be used to measure data usage on a fine grained time scale.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param template Template used to match networks. See {@link NetworkTemplate}.
+ * @param startTime Start of period, in milliseconds since the Unix epoch, see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period, in milliseconds since the Unix epoch, see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Statistics which is described above.
+ * @hide
+ */
+ @NonNull
+ @SystemApi(client = MODULE_LIBRARIES)
+ @WorkerThread
+ public NetworkStats queryDetailsForDevice(@NonNull NetworkTemplate template,
+ long startTime, long endTime) {
+ Objects.requireNonNull(template);
+ try {
+ final NetworkStats result =
+ new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+ result.startHistoryDeviceEnumeration();
+ return result;
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+
+ return null; // To make the compiler happy.
+ }
+
+ /**
+ * Query network usage statistics details for a given uid.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
+ */
+ @NonNull
+ @WorkerThread
+ public NetworkStats queryDetailsForUid(int networkType, @Nullable String subscriberId,
+ long startTime, long endTime, int uid) throws SecurityException {
+ return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
+ NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
+ }
+
+ /** @hide */
+ @NonNull
+ public NetworkStats queryDetailsForUid(@NonNull NetworkTemplate template,
+ long startTime, long endTime, int uid) throws SecurityException {
+ return queryDetailsForUidTagState(template, startTime, endTime, uid,
+ NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
+ }
+
+ /**
+ * Query network usage statistics details for a given uid and tag.
+ *
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ * Only usable for uids belonging to calling user. Result is not aggregated over time.
+ * This means buckets' start and end timestamps are going to be between 'startTime' and
+ * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+ * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+ * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+ * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+ * interpolate across partial buckets. Since bucket length is in the order of hours, this
+ * method cannot be used to measure data usage on a fine grained time scale.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param uid UID of app
+ * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+ * across all the tags.
+ * @return Statistics which is described above.
+ * @throws SecurityException if permissions are insufficient to read network statistics.
+ */
+ @NonNull
+ @WorkerThread
+ public NetworkStats queryDetailsForUidTag(int networkType, @Nullable String subscriberId,
+ long startTime, long endTime, int uid, int tag) throws SecurityException {
+ return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
+ tag, NetworkStats.Bucket.STATE_ALL);
+ }
+
+ /**
+ * Query network usage statistics details for a given uid, tag, and state.
+ *
+ * Only usable for uids belonging to calling user. Result is not aggregated over time.
+ * This means buckets' start and end timestamps are going to be between 'startTime' and
+ * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+ * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+ * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+ * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+ * interpolate across partial buckets. Since bucket length is in the order of hours, this
+ * method cannot be used to measure data usage on a fine grained time scale.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param uid UID of app
+ * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+ * across all the tags.
+ * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
+ * traffic from all states.
+ * @return Statistics which is described above.
+ * @throws SecurityException if permissions are insufficient to read network statistics.
+ */
+ @NonNull
+ @WorkerThread
+ public NetworkStats queryDetailsForUidTagState(int networkType, @Nullable String subscriberId,
+ long startTime, long endTime, int uid, int tag, int state) throws SecurityException {
+ NetworkTemplate template;
+ template = createTemplate(networkType, subscriberId);
+
+ return queryDetailsForUidTagState(template, startTime, endTime, uid, tag, state);
+ }
+
+ /**
+ * Query network usage statistics details for a given template, uid, tag, and state.
+ *
+ * Only usable for uids belonging to calling user. Result is not aggregated over time.
+ * This means buckets' start and end timestamps are going to be between 'startTime' and
+ * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+ * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+ * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+ * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+ * interpolate across partial buckets. Since bucket length is in the order of hours, this
+ * method cannot be used to measure data usage on a fine grained time scale.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param template Template used to match networks. See {@link NetworkTemplate}.
+ * @param startTime Start of period, in milliseconds since the Unix epoch, see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period, in milliseconds since the Unix epoch, see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param uid UID of app
+ * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+ * across all the tags.
+ * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
+ * traffic from all states.
+ * @return Statistics which is described above.
+ * @hide
+ */
+ @NonNull
+ @SystemApi(client = MODULE_LIBRARIES)
+ @WorkerThread
+ public NetworkStats queryDetailsForUidTagState(@NonNull NetworkTemplate template,
+ long startTime, long endTime, int uid, int tag, int state) throws SecurityException {
+ Objects.requireNonNull(template);
+ try {
+ final NetworkStats result = new NetworkStats(
+ mContext, template, mFlags, startTime, endTime, mService);
+ result.startHistoryUidEnumeration(uid, tag, state);
+ return result;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error while querying stats for uid=" + uid + " tag=" + tag
+ + " state=" + state, e);
+ e.rethrowFromSystemServer();
+ }
+
+ return null; // To make the compiler happy.
+ }
+
+ /**
+ * Query network usage statistics details. Result filtered to include only uids belonging to
+ * calling user. Result is aggregated over state but not aggregated over time, uid, tag,
+ * metered, nor roaming. This means buckets' start and end timestamps are going to be between
+ * 'startTime' and 'endTime' parameters. State is going to be
+ * {@link NetworkStats.Bucket#STATE_ALL}, uid will vary,
+ * tag {@link NetworkStats.Bucket#TAG_NONE},
+ * default network is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL},
+ * and roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+ * interpolate across partial buckets. Since bucket length is in the order of hours, this
+ * method cannot be used to measure data usage on a fine grained time scale.
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Statistics object or null if permissions are insufficient or error happened during
+ * statistics collection.
+ */
+ @WorkerThread
+ public NetworkStats queryDetails(int networkType, @Nullable String subscriberId, long startTime,
+ long endTime) throws SecurityException, RemoteException {
+ NetworkTemplate template;
+ try {
+ template = createTemplate(networkType, subscriberId);
+ } catch (IllegalArgumentException e) {
+ if (DBG) Log.e(TAG, "Cannot create template", e);
+ return null;
+ }
+
+ NetworkStats result;
+ result = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
+ result.startUserUidEnumeration();
+ return result;
+ }
+
+ /**
+ * Query realtime mobile network usage statistics.
+ *
+ * Return a snapshot of current UID network statistics, as it applies
+ * to the mobile radios of the device. The snapshot will include any
+ * tethering traffic, video calling data usage and count of
+ * network operations set by {@link TrafficStats#incrementOperationCount}
+ * made over a mobile radio.
+ * The snapshot will not include any statistics that cannot be seen by
+ * the kernel, e.g. statistics reported by {@link NetworkStatsProvider}s.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_STACK})
+ @NonNull public android.net.NetworkStats getMobileUidStats() {
+ try {
+ return mService.getUidStatsForTransport(TRANSPORT_CELLULAR);
+ } catch (RemoteException e) {
+ if (DBG) Log.d(TAG, "Remote exception when get Mobile uid stats");
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Query realtime Wi-Fi network usage statistics.
+ *
+ * Return a snapshot of current UID network statistics, as it applies
+ * to the Wi-Fi radios of the device. The snapshot will include any
+ * tethering traffic, video calling data usage and count of
+ * network operations set by {@link TrafficStats#incrementOperationCount}
+ * made over a Wi-Fi radio.
+ * The snapshot will not include any statistics that cannot be seen by
+ * the kernel, e.g. statistics reported by {@link NetworkStatsProvider}s.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_STACK})
+ @NonNull public android.net.NetworkStats getWifiUidStats() {
+ try {
+ return mService.getUidStatsForTransport(TRANSPORT_WIFI);
+ } catch (RemoteException e) {
+ if (DBG) Log.d(TAG, "Remote exception when get WiFi uid stats");
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Registers to receive notifications about data usage on specified networks.
+ *
+ * <p>The callbacks will continue to be called as long as the process is alive or
+ * {@link #unregisterUsageCallback} is called.
+ *
+ * @param template Template used to match networks. See {@link NetworkTemplate}.
+ * @param thresholdBytes Threshold in bytes to be notified on. Provided values lower than 2MiB
+ * will be clamped for callers except callers with the NETWORK_STACK
+ * permission.
+ * @param executor The executor on which callback will be invoked. The provided {@link Executor}
+ * must run callback sequentially, otherwise the order of callbacks cannot be
+ * guaranteed.
+ * @param callback The {@link UsageCallback} that the system will call when data usage
+ * has exceeded the specified threshold.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_STACK}, conditional = true)
+ public void registerUsageCallback(@NonNull NetworkTemplate template, long thresholdBytes,
+ @NonNull @CallbackExecutor Executor executor, @NonNull UsageCallback callback) {
+ Objects.requireNonNull(template, "NetworkTemplate cannot be null");
+ Objects.requireNonNull(callback, "UsageCallback cannot be null");
+ Objects.requireNonNull(executor, "Executor cannot be null");
+
+ final DataUsageRequest request = new DataUsageRequest(DataUsageRequest.REQUEST_ID_UNSET,
+ template, thresholdBytes);
+ try {
+ final UsageCallbackWrapper callbackWrapper =
+ new UsageCallbackWrapper(executor, callback);
+ callback.request = mService.registerUsageCallback(
+ mContext.getOpPackageName(), request, callbackWrapper);
+ if (DBG) Log.d(TAG, "registerUsageCallback returned " + callback.request);
+
+ if (callback.request == null) {
+ Log.e(TAG, "Request from callback is null; should not happen");
+ }
+ } catch (RemoteException e) {
+ if (DBG) Log.d(TAG, "Remote exception when registering callback");
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Registers to receive notifications about data usage on specified networks.
+ *
+ * <p>The callbacks will continue to be called as long as the process is live or
+ * {@link #unregisterUsageCallback} is called.
+ *
+ * @param networkType Type of network to monitor. Either
+ {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when registering for the mobile network type to receive
+ * notifications for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param thresholdBytes Threshold in bytes to be notified on.
+ * @param callback The {@link UsageCallback} that the system will call when data usage
+ * has exceeded the specified threshold.
+ */
+ public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+ long thresholdBytes, @NonNull UsageCallback callback) {
+ registerUsageCallback(networkType, subscriberId, thresholdBytes, callback,
+ null /* handler */);
+ }
+
+ /**
+ * Registers to receive notifications about data usage on specified networks.
+ *
+ * <p>The callbacks will continue to be called as long as the process is live or
+ * {@link #unregisterUsageCallback} is called.
+ *
+ * @param networkType Type of network to monitor. Either
+ {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when registering for the mobile network type to receive
+ * notifications for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param thresholdBytes Threshold in bytes to be notified on.
+ * @param callback The {@link UsageCallback} that the system will call when data usage
+ * has exceeded the specified threshold.
+ * @param handler to dispatch callback events through, otherwise if {@code null} it uses
+ * the calling thread.
+ */
+ public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+ long thresholdBytes, @NonNull UsageCallback callback, @Nullable Handler handler) {
+ NetworkTemplate template = createTemplate(networkType, subscriberId);
+ if (DBG) {
+ Log.d(TAG, "registerUsageCallback called with: {"
+ + " networkType=" + networkType
+ + " subscriberId=" + subscriberId
+ + " thresholdBytes=" + thresholdBytes
+ + " }");
+ }
+
+ final Executor executor = handler == null ? r -> r.run() : r -> handler.post(r);
+
+ registerUsageCallback(template, thresholdBytes, executor, callback);
+ }
+
+ /**
+ * Unregisters callbacks on data usage.
+ *
+ * @param callback The {@link UsageCallback} used when registering.
+ */
+ public void unregisterUsageCallback(@NonNull UsageCallback callback) {
+ if (callback == null || callback.request == null
+ || callback.request.requestId == DataUsageRequest.REQUEST_ID_UNSET) {
+ throw new IllegalArgumentException("Invalid UsageCallback");
+ }
+ try {
+ mService.unregisterUsageRequest(callback.request);
+ } catch (RemoteException e) {
+ if (DBG) Log.d(TAG, "Remote exception when unregistering callback");
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Base class for usage callbacks. Should be extended by applications wanting notifications.
+ */
+ public static abstract class UsageCallback {
+ /**
+ * Called when data usage has reached the given threshold.
+ *
+ * Called by {@code NetworkStatsService} when the registered threshold is reached.
+ * If a caller implements {@link #onThresholdReached(NetworkTemplate)}, the system
+ * will not call {@link #onThresholdReached(int, String)}.
+ *
+ * @param template The {@link NetworkTemplate} that associated with this callback.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public void onThresholdReached(@NonNull NetworkTemplate template) {
+ // Backward compatibility for those who didn't override this function.
+ final int networkType = networkTypeForTemplate(template);
+ if (networkType != ConnectivityManager.TYPE_NONE) {
+ final String subscriberId = template.getSubscriberIds().isEmpty() ? null
+ : template.getSubscriberIds().iterator().next();
+ onThresholdReached(networkType, subscriberId);
+ }
+ }
+
+ /**
+ * Called when data usage has reached the given threshold.
+ */
+ public abstract void onThresholdReached(int networkType, @Nullable String subscriberId);
+
+ /**
+ * @hide used for internal bookkeeping
+ */
+ private DataUsageRequest request;
+
+ /**
+ * Get network type from a template if feasible.
+ *
+ * @param template the target {@link NetworkTemplate}.
+ * @return legacy network type, only supports for the types which is already supported in
+ * {@link #registerUsageCallback(int, String, long, UsageCallback, Handler)}.
+ * {@link ConnectivityManager#TYPE_NONE} for other types.
+ */
+ private static int networkTypeForTemplate(@NonNull NetworkTemplate template) {
+ switch (template.getMatchRule()) {
+ case NetworkTemplate.MATCH_MOBILE:
+ return ConnectivityManager.TYPE_MOBILE;
+ case NetworkTemplate.MATCH_WIFI:
+ return ConnectivityManager.TYPE_WIFI;
+ default:
+ return ConnectivityManager.TYPE_NONE;
+ }
+ }
+ }
+
+ /**
+ * Registers a custom provider of {@link android.net.NetworkStats} to provide network statistics
+ * to the system. To unregister, invoke {@link #unregisterNetworkStatsProvider}.
+ * Note that no de-duplication of statistics between providers is performed, so each provider
+ * must only report network traffic that is not being reported by any other provider. Also note
+ * that the provider cannot be re-registered after unregistering.
+ *
+ * @param tag a human readable identifier of the custom network stats provider. This is only
+ * used for debugging.
+ * @param provider the subclass of {@link NetworkStatsProvider} that needs to be
+ * registered to the system.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_STATS_PROVIDER,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
+ public void registerNetworkStatsProvider(
+ @NonNull String tag,
+ @NonNull NetworkStatsProvider provider) {
+ try {
+ if (provider.getProviderCallbackBinder() != null) {
+ throw new IllegalArgumentException("provider is already registered");
+ }
+ final INetworkStatsProviderCallback cbBinder =
+ mService.registerNetworkStatsProvider(tag, provider.getProviderBinder());
+ provider.setProviderCallbackBinder(cbBinder);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Unregisters an instance of {@link NetworkStatsProvider}.
+ *
+ * @param provider the subclass of {@link NetworkStatsProvider} that needs to be
+ * unregistered to the system.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_STATS_PROVIDER,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
+ public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) {
+ try {
+ provider.getProviderCallbackBinderOrThrow().unregister();
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ private static NetworkTemplate createTemplate(int networkType, @Nullable String subscriberId) {
+ final NetworkTemplate template;
+ switch (networkType) {
+ case ConnectivityManager.TYPE_MOBILE:
+ template = subscriberId == null
+ ? new NetworkTemplate.Builder(MATCH_MOBILE)
+ .setMeteredness(METERED_YES).build()
+ : new NetworkTemplate.Builder(MATCH_MOBILE)
+ .setMeteredness(METERED_YES)
+ .setSubscriberIds(Set.of(subscriberId)).build();
+ break;
+ case ConnectivityManager.TYPE_WIFI:
+ template = TextUtils.isEmpty(subscriberId)
+ ? new NetworkTemplate.Builder(MATCH_WIFI).build()
+ : new NetworkTemplate.Builder(MATCH_WIFI)
+ .setSubscriberIds(Set.of(subscriberId)).build();
+ break;
+ default:
+ throw new IllegalArgumentException("Cannot create template for network type "
+ + networkType + ", subscriberId '"
+ + NetworkIdentityUtils.scrubSubscriberId(subscriberId) + "'.");
+ }
+ return template;
+ }
+
+ /**
+ * Notify {@code NetworkStatsService} about network status changed.
+ *
+ * Notifies NetworkStatsService of network state changes for data usage accounting purposes.
+ *
+ * To avoid races that attribute data usage to wrong network, such as new network with
+ * the same interface after SIM hot-swap, this function will not return until
+ * {@code NetworkStatsService} finishes its work of retrieving traffic statistics from
+ * all data sources.
+ *
+ * @param defaultNetworks the list of all networks that could be used by network traffic that
+ * does not explicitly select a network.
+ * @param networkStateSnapshots a list of {@link NetworkStateSnapshot}s, one for
+ * each network that is currently connected.
+ * @param activeIface the active (i.e., connected) default network interface for the calling
+ * uid. Used to determine on which network future calls to
+ * {@link android.net.TrafficStats#incrementOperationCount} applies to.
+ * @param underlyingNetworkInfos the list of underlying network information for all
+ * currently-connected VPNs.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_STACK})
+ public void notifyNetworkStatus(
+ @NonNull List<Network> defaultNetworks,
+ @NonNull List<NetworkStateSnapshot> networkStateSnapshots,
+ @Nullable String activeIface,
+ @NonNull List<UnderlyingNetworkInfo> underlyingNetworkInfos) {
+ try {
+ Objects.requireNonNull(defaultNetworks);
+ Objects.requireNonNull(networkStateSnapshots);
+ Objects.requireNonNull(underlyingNetworkInfos);
+ mService.notifyNetworkStatus(defaultNetworks.toArray(new Network[0]),
+ networkStateSnapshots.toArray(new NetworkStateSnapshot[0]), activeIface,
+ underlyingNetworkInfos.toArray(new UnderlyingNetworkInfo[0]));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private static class UsageCallbackWrapper extends IUsageCallback.Stub {
+ // Null if unregistered.
+ private volatile UsageCallback mCallback;
+
+ private final Executor mExecutor;
+
+ UsageCallbackWrapper(@NonNull Executor executor, @NonNull UsageCallback callback) {
+ mCallback = callback;
+ mExecutor = executor;
+ }
+
+ @Override
+ public void onThresholdReached(DataUsageRequest request) {
+ // Copy it to a local variable in case mCallback changed inside the if condition.
+ final UsageCallback callback = mCallback;
+ if (callback != null) {
+ mExecutor.execute(() -> callback.onThresholdReached(request.template));
+ } else {
+ Log.e(TAG, "onThresholdReached with released callback for " + request);
+ }
+ }
+
+ @Override
+ public void onCallbackReleased(DataUsageRequest request) {
+ if (DBG) Log.d(TAG, "callback released for " + request);
+ mCallback = null;
+ }
+ }
+
+ /**
+ * Mark given UID as being in foreground for stats purposes.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_STACK})
+ public void noteUidForeground(int uid, boolean uidForeground) {
+ try {
+ mService.noteUidForeground(uid, uidForeground);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set default value of global alert bytes, the value will be clamped to [128kB, 2MB].
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ Manifest.permission.NETWORK_STACK})
+ public void setDefaultGlobalAlert(long alertBytes) {
+ try {
+ // TODO: Sync internal naming with the API surface.
+ mService.advisePersistThreshold(alertBytes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Force update of statistics.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_STACK})
+ public void forceUpdate() {
+ try {
+ mService.forceUpdate();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set the warning and limit to all registered custom network stats providers.
+ * Note that invocation of any interface will be sent to all providers.
+ *
+ * Asynchronicity notes : because traffic may be happening on the device at the same time, it
+ * doesn't make sense to wait for the warning and limit to be set – a caller still wouldn't
+ * know when exactly it was effective. All that can matter is that it's done quickly. Also,
+ * this method can't fail, so there is no status to return. All providers will see the new
+ * values soon.
+ * As such, this method returns immediately and sends the warning and limit to all providers
+ * as soon as possible through a one-way binder call.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_STACK})
+ public void setStatsProviderWarningAndLimitAsync(@NonNull String iface, long warning,
+ long limit) {
+ try {
+ mService.setStatsProviderWarningAndLimitAsync(iface, warning, limit);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get a RAT type representative of a group of RAT types for network statistics.
+ *
+ * Collapse the given Radio Access Technology (RAT) type into a bucket that
+ * is representative of the original RAT type for network statistics. The
+ * mapping mostly corresponds to {@code TelephonyManager#NETWORK_CLASS_BIT_MASK_*}
+ * but with adaptations specific to the virtual types introduced by
+ * networks stats.
+ *
+ * @param ratType An integer defined in {@code TelephonyManager#NETWORK_TYPE_*}.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static int getCollapsedRatType(int ratType) {
+ switch (ratType) {
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_GSM:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ return TelephonyManager.NETWORK_TYPE_GSM;
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
+ return TelephonyManager.NETWORK_TYPE_UMTS;
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ case TelephonyManager.NETWORK_TYPE_IWLAN:
+ return TelephonyManager.NETWORK_TYPE_LTE;
+ case TelephonyManager.NETWORK_TYPE_NR:
+ return TelephonyManager.NETWORK_TYPE_NR;
+ // Virtual RAT type for 5G NSA mode, see
+ // {@link NetworkStatsManager#NETWORK_TYPE_5G_NSA}.
+ case NetworkStatsManager.NETWORK_TYPE_5G_NSA:
+ return NetworkStatsManager.NETWORK_TYPE_5G_NSA;
+ default:
+ return TelephonyManager.NETWORK_TYPE_UNKNOWN;
+ }
+ }
+}
diff --git a/android-34/android/app/usage/UsageStatsManagerInternal.java b/android-34/android/app/usage/UsageStatsManagerInternal.java
deleted file mode 100644
index fc56511..0000000
--- a/android-34/android/app/usage/UsageStatsManagerInternal.java
+++ /dev/null
@@ -1,427 +0,0 @@
-/**
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-
-package android.app.usage;
-
-import android.annotation.CurrentTimeMillisLong;
-import android.annotation.ElapsedRealtimeLong;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.app.ActivityManager.ProcessState;
-import android.app.usage.UsageStatsManager.StandbyBuckets;
-import android.content.ComponentName;
-import android.content.LocusId;
-import android.content.res.Configuration;
-import android.os.IBinder;
-import android.os.SystemClock;
-import android.os.UserHandle;
-import android.os.UserManager;
-
-import java.util.List;
-import java.util.Set;
-
-/**
- * UsageStatsManager local system service interface.
- *
- * {@hide} Only for use within the system server.
- */
-public abstract class UsageStatsManagerInternal {
-
- /**
- * Reports an event to the UsageStatsManager. <br/>
- * <em>Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
- * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
- * then this event will be added to a queue and processed once the device is unlocked.</em>
- *
- * @param component The component for which this event occurred.
- * @param userId The user id to which the component belongs to.
- * @param eventType The event that occurred. Valid values can be found at
- * {@link UsageEvents}
- * @param instanceId For activity, hashCode of ActivityRecord's appToken.
- * For non-activity, it is not used.
- * @param taskRoot For activity, the name of the package at the root of the task
- * For non-activity, it is not used.
- */
- public abstract void reportEvent(ComponentName component, @UserIdInt int userId, int eventType,
- int instanceId, ComponentName taskRoot);
-
- /**
- * Reports an event to the UsageStatsManager. <br/>
- * <em>Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
- * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
- * then this event will be added to a queue and processed once the device is unlocked.</em>
- *
- * @param packageName The package for which this event occurred.
- * @param userId The user id to which the component belongs to.
- * @param eventType The event that occurred. Valid values can be found at
- * {@link UsageEvents}
- */
- public abstract void reportEvent(String packageName, @UserIdInt int userId, int eventType);
-
- /**
- * Reports a configuration change to the UsageStatsManager. <br/>
- * <em>Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
- * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
- * then this event will be added to a queue and processed once the device is unlocked.</em>
- *
- * @param config The new device configuration.
- */
- public abstract void reportConfigurationChange(Configuration config, @UserIdInt int userId);
-
- /**
- * Reports that an application has posted an interruptive notification. <br/>
- * <em>Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
- * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
- * then this event will be added to a queue and processed once the device is unlocked.</em>
- *
- * @param packageName The package name of the app that posted the notification
- * @param channelId The ID of the NotificationChannel to which the notification was posted
- * @param userId The user in which the notification was posted
- */
- public abstract void reportInterruptiveNotification(String packageName, String channelId,
- @UserIdInt int userId);
-
- /**
- * Reports that an action equivalent to a ShortcutInfo is taken by the user. <br/>
- * <em>Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
- * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
- * then this event will be added to a queue and processed once the device is unlocked.</em>
- *
- * @param packageName The package name of the shortcut publisher
- * @param shortcutId The ID of the shortcut in question
- * @param userId The user in which the content provider was accessed.
- *
- * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
- */
- public abstract void reportShortcutUsage(String packageName, String shortcutId,
- @UserIdInt int userId);
-
- /**
- * Reports that a content provider has been accessed by a foreground app.
- * @param name The authority of the content provider
- * @param pkgName The package name of the content provider
- * @param userId The user in which the content provider was accessed.
- */
- public abstract void reportContentProviderUsage(String name, String pkgName,
- @UserIdInt int userId);
-
-
- /**
- * Reports locusId update for a given activity.
- *
- * @param activity The component name of the app.
- * @param userId The user id of who uses the app.
- * @param locusId The locusId a unique, stable id that identifies this activity.
- * @param appToken ActivityRecord's appToken.
- * {@link UsageEvents}
- * @hide
- */
- public abstract void reportLocusUpdate(@NonNull ComponentName activity, @UserIdInt int userId,
- @Nullable LocusId locusId, @NonNull IBinder appToken);
-
- /**
- * Prepares the UsageStatsService for shutdown.
- */
- public abstract void prepareShutdown();
-
- /**
- * When the device power button is long pressed for 3.5 seconds, prepareForPossibleShutdown()
- * is called.
- */
- public abstract void prepareForPossibleShutdown();
-
- /**
- * Returns true if the app has not been used for a certain amount of time. How much time?
- * Could be hours, could be days, who knows?
- *
- * @param packageName
- * @param uidForAppId The uid of the app, which will be used for its app id
- * @param userId
- * @return
- */
- public abstract boolean isAppIdle(String packageName, int uidForAppId, @UserIdInt int userId);
-
- /**
- * Returns the app standby bucket that the app is currently in. This accessor does
- * <em>not</em> obfuscate instant apps.
- *
- * @param packageName
- * @param userId
- * @param nowElapsed The current time, in the elapsedRealtime time base
- * @return the AppStandby bucket code the app currently resides in. If the app is
- * unknown in the given user, STANDBY_BUCKET_NEVER is returned.
- */
- @StandbyBuckets public abstract int getAppStandbyBucket(String packageName,
- @UserIdInt int userId, long nowElapsed);
-
- /**
- * Returns all of the uids for a given user where all packages associating with that uid
- * are in the app idle state -- there are no associated apps that are not idle. This means
- * all of the returned uids can be safely considered app idle.
- */
- public abstract int[] getIdleUidsForUser(@UserIdInt int userId);
-
- /** Backup/Restore API */
- public abstract byte[] getBackupPayload(@UserIdInt int userId, String key);
-
- /**
- * ?
- * @param userId
- * @param key
- * @param payload
- */
- public abstract void applyRestoredPayload(@UserIdInt int userId, String key, byte[] payload);
-
- /**
- * Called by DevicePolicyManagerService to inform that a new admin has been added.
- *
- * @param packageName the package in which the admin component is part of.
- * @param userId the userId in which the admin has been added.
- */
- public abstract void onActiveAdminAdded(String packageName, int userId);
-
- /**
- * Called by DevicePolicyManagerService to inform about the active admins in an user.
- *
- * @param adminApps the set of active admins in {@param userId} or null if there are none.
- * @param userId the userId to which the admin apps belong.
- */
- public abstract void setActiveAdminApps(Set<String> adminApps, int userId);
-
- /**
- * Called by DevicePolicyManagerService to inform about the protected packages for a user.
- * User control will be disabled for protected packages.
- *
- * @param packageNames the set of protected packages for {@code userId}.
- * @param userId the userId to which the protected packages belong.
- */
- public abstract void setAdminProtectedPackages(@Nullable Set<String> packageNames,
- @UserIdInt int userId);
-
- /**
- * Called by DevicePolicyManagerService during boot to inform that admin data is loaded and
- * pushed to UsageStatsService.
- */
- public abstract void onAdminDataAvailable();
-
- /**
- * Return usage stats.
- *
- * @param obfuscateInstantApps whether instant app package names need to be obfuscated in the
- * result.
- */
- public abstract List<UsageStats> queryUsageStatsForUser(@UserIdInt int userId, int interval,
- long beginTime, long endTime, boolean obfuscateInstantApps);
-
- /**
- * Returns the events for the user in the given time period.
- *
- * @param flags defines the visibility of certain usage events - see flags defined in
- * {@link UsageEvents}.
- */
- public abstract UsageEvents queryEventsForUser(@UserIdInt int userId, long beginTime,
- long endTime, int flags);
-
- /**
- * Used to persist the last time a job was run for this app, in order to make decisions later
- * whether a job should be deferred until later. The time passed in should be in elapsed
- * realtime since boot.
- * @param packageName the app that executed a job.
- * @param userId the user associated with the job.
- * @param elapsedRealtime the time when the job was executed, in elapsed realtime millis since
- * boot.
- */
- public abstract void setLastJobRunTime(String packageName, @UserIdInt int userId,
- long elapsedRealtime);
-
- /** Returns the estimated time that the app will be launched, in milliseconds since epoch. */
- @CurrentTimeMillisLong
- public abstract long getEstimatedPackageLaunchTime(String packageName, @UserIdInt int userId);
-
- /**
- * Returns the time in millis since a job was executed for this app, in elapsed realtime
- * timebase. This value can be larger than the current elapsed realtime if the job was executed
- * before the device was rebooted. The default value is {@link Long#MAX_VALUE}.
- * @param packageName the app you're asking about.
- * @param userId the user associated with the job.
- * @return the time in millis since a job was last executed for the app, provided it was
- * indicated here before by a call to {@link #setLastJobRunTime(String, int, long)}.
- */
- public abstract long getTimeSinceLastJobRun(String packageName, @UserIdInt int userId);
-
- /**
- * Report a few data points about an app's job state at the current time.
- *
- * @param packageName the app whose job state is being described
- * @param userId which user the app is associated with
- * @param numDeferredJobs the number of pending jobs that were deferred
- * due to bucketing policy
- * @param timeSinceLastJobRun number of milliseconds since the last time one of
- * this app's jobs was executed
- */
- public abstract void reportAppJobState(String packageName, @UserIdInt int userId,
- int numDeferredJobs, long timeSinceLastJobRun);
-
- /**
- * Report a sync that was scheduled.
- *
- * @param packageName name of the package that owns the sync adapter.
- * @param userId which user the app is associated with
- * @param exempted is sync app standby exempted
- */
- public abstract void reportSyncScheduled(String packageName, @UserIdInt int userId,
- boolean exempted);
-
- /**
- * Report a sync that was scheduled by a foreground app is about to be executed.
- *
- * @param packageName name of the package that owns the sync adapter.
- * @param userId which user the app is associated with
- */
- public abstract void reportExemptedSyncStart(String packageName, @UserIdInt int userId);
-
- /**
- * Returns an object describing the app usage limit for the given package which was set via
- * {@link UsageStatsManager#registerAppUsageLimitObserver}.
- * If there are multiple limits that apply to the package, the one with the smallest
- * time remaining will be returned.
- *
- * @param packageName name of the package whose app usage limit will be returned
- * @param user the user associated with the limit
- * @return an {@link AppUsageLimitData} object describing the app time limit containing
- * the given package, with the smallest time remaining.
- */
- public abstract AppUsageLimitData getAppUsageLimit(String packageName, UserHandle user);
-
- /** A class which is used to share the usage limit data for an app or a group of apps. */
- public static class AppUsageLimitData {
- private final long mTotalUsageLimit;
- private final long mUsageRemaining;
-
- public AppUsageLimitData(long totalUsageLimit, long usageRemaining) {
- this.mTotalUsageLimit = totalUsageLimit;
- this.mUsageRemaining = usageRemaining;
- }
-
- public long getTotalUsageLimit() {
- return mTotalUsageLimit;
- }
- public long getUsageRemaining() {
- return mUsageRemaining;
- }
- }
-
- /**
- * Called by {@link com.android.server.usage.UsageStatsIdleService} when the device is idle to
- * prune usage stats data for uninstalled packages.
- *
- * @param userId the user associated with the job
- * @return {@code true} if the pruning was successful, {@code false} otherwise
- */
- public abstract boolean pruneUninstalledPackagesData(@UserIdInt int userId);
-
- /**
- * Called by {@link com.android.server.usage.UsageStatsIdleService} between 24 to 48 hours of
- * when the user is first unlocked to update the usage stats package mappings data that might
- * be stale or have existed from a restore and belongs to packages that are not installed for
- * this user anymore.
- *
- * @param userId The user to update
- * @return {@code true} if the updating was successful, {@code false} otherwise
- */
- public abstract boolean updatePackageMappingsData(@UserIdInt int userId);
-
- /**
- * Listener interface for usage events.
- */
- public interface UsageEventListener {
- /** Callback to inform listeners of a new usage event. */
- void onUsageEvent(@UserIdInt int userId, @NonNull UsageEvents.Event event);
- }
-
- /** Register a listener that will be notified of every new usage event. */
- public abstract void registerListener(@NonNull UsageEventListener listener);
-
- /** Unregister a listener from being notified of every new usage event. */
- public abstract void unregisterListener(@NonNull UsageEventListener listener);
-
- /**
- * Listener interface for estimated launch time changes.
- */
- public interface EstimatedLaunchTimeChangedListener {
- /** Callback to inform listeners when estimated launch times change. */
- void onEstimatedLaunchTimeChanged(@UserIdInt int userId, @NonNull String packageName,
- @CurrentTimeMillisLong long newEstimatedLaunchTime);
- }
-
- /** Register a listener that will be notified of every estimated launch time change. */
- public abstract void registerLaunchTimeChangedListener(
- @NonNull EstimatedLaunchTimeChangedListener listener);
-
- /** Unregister a listener from being notified of every estimated launch time change. */
- public abstract void unregisterLaunchTimeChangedListener(
- @NonNull EstimatedLaunchTimeChangedListener listener);
-
- /**
- * Reports a broadcast dispatched event to the UsageStatsManager.
- *
- * @param sourceUid uid of the package that sent the broadcast.
- * @param targetPackage name of the package that the broadcast is targeted to.
- * @param targetUser user that {@code targetPackage} belongs to.
- * @param idForResponseEvent ID to be used for recording any response events corresponding
- * to this broadcast.
- * @param timestampMs time (in millis) when the broadcast was dispatched, in
- * {@link SystemClock#elapsedRealtime()} timebase.
- * @param targetUidProcState process state of the uid that the broadcast is targeted to.
- */
- public abstract void reportBroadcastDispatched(int sourceUid, @NonNull String targetPackage,
- @NonNull UserHandle targetUser, long idForResponseEvent,
- @ElapsedRealtimeLong long timestampMs, @ProcessState int targetUidProcState);
-
- /**
- * Reports a notification posted event to the UsageStatsManager.
- *
- * @param packageName name of the package which posted the notification.
- * @param user user that {@code packageName} belongs to.
- * @param timestampMs time (in millis) when the notification was posted, in
- * {@link SystemClock#elapsedRealtime()} timebase.
- */
- public abstract void reportNotificationPosted(@NonNull String packageName,
- @NonNull UserHandle user, @ElapsedRealtimeLong long timestampMs);
-
- /**
- * Reports a notification updated event to the UsageStatsManager.
- *
- * @param packageName name of the package which updated the notification.
- * @param user user that {@code packageName} belongs to.
- * @param timestampMs time (in millis) when the notification was updated, in
- * {@link SystemClock#elapsedRealtime()} timebase.
- */
- public abstract void reportNotificationUpdated(@NonNull String packageName,
- @NonNull UserHandle user, @ElapsedRealtimeLong long timestampMs);
-
- /**
- * Reports a notification removed event to the UsageStatsManager.
- *
- * @param packageName name of the package which removed the notification.
- * @param user user that {@code packageName} belongs to.
- * @param timestampMs time (in millis) when the notification was removed, in
- * {@link SystemClock#elapsedRealtime()} timebase.
- */
- public abstract void reportNotificationRemoved(@NonNull String packageName,
- @NonNull UserHandle user, @ElapsedRealtimeLong long timestampMs);
-}
diff --git a/android-34/android/bluetooth/Attributable.java b/android-34/android/bluetooth/Attributable.java
new file mode 100644
index 0000000..539bb49
--- /dev/null
+++ b/android-34/android/bluetooth/Attributable.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.AttributionSource;
+
+import java.util.List;
+
+/**
+ * Marker interface for a class which can have an {@link AttributionSource}
+ * assigned to it; these are typically {@link android.os.Parcelable} classes
+ * which need to be updated after crossing Binder transaction boundaries.
+ *
+ * @hide
+ */
+public interface Attributable {
+ /** @hide */
+ void setAttributionSource(@NonNull AttributionSource attributionSource);
+
+ /** @hide */
+ static @Nullable <T extends Attributable> T setAttributionSource(
+ @Nullable T attributable,
+ @NonNull AttributionSource attributionSource) {
+ if (attributable != null) {
+ attributable.setAttributionSource(attributionSource);
+ }
+ return attributable;
+ }
+
+ /** @hide */
+ static @Nullable <T extends Attributable> List<T> setAttributionSource(
+ @Nullable List<T> attributableList,
+ @NonNull AttributionSource attributionSource) {
+ if (attributableList != null) {
+ final int size = attributableList.size();
+ for (int i = 0; i < size; i++) {
+ setAttributionSource(attributableList.get(i), attributionSource);
+ }
+ }
+ return attributableList;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothA2dp.java b/android-34/android/bluetooth/BluetoothA2dp.java
new file mode 100644
index 0000000..5eabc4b
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothA2dp.java
@@ -0,0 +1,1210 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Bluetooth A2DP
+ * profile.
+ *
+ * <p>BluetoothA2dp is a proxy object for controlling the Bluetooth A2DP
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothA2dp proxy object.
+ *
+ * <p> Android only supports one connected Bluetooth A2dp device at a time.
+ * Each method is protected with its appropriate permission.
+ */
+public final class BluetoothA2dp implements BluetoothProfile {
+ private static final String TAG = "BluetoothA2dp";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the A2DP
+ * profile.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in the Playing state of the A2DP
+ * profile.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_PLAYING}, {@link #STATE_NOT_PLAYING},
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PLAYING_STATE_CHANGED =
+ "android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED";
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AVRCP_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.a2dp.profile.action.AVRCP_CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the selection of a connected device as active.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.a2dp.profile.action.ACTIVE_DEVICE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in the Audio Codec state of the
+ * A2DP Source profile.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothCodecStatus#EXTRA_CODEC_STATUS} - The codec status. </li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device if the device is currently
+ * connected, otherwise it is not included.</li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_CODEC_CONFIG_CHANGED =
+ "android.bluetooth.a2dp.profile.action.CODEC_CONFIG_CHANGED";
+
+ /**
+ * A2DP sink device is streaming music. This state can be one of
+ * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+ * {@link #ACTION_PLAYING_STATE_CHANGED} intent.
+ */
+ public static final int STATE_PLAYING = 10;
+
+ /**
+ * A2DP sink device is NOT streaming music. This state can be one of
+ * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+ * {@link #ACTION_PLAYING_STATE_CHANGED} intent.
+ */
+ public static final int STATE_NOT_PLAYING = 11;
+
+ /** @hide */
+ @IntDef(prefix = "OPTIONAL_CODECS_", value = {
+ OPTIONAL_CODECS_SUPPORT_UNKNOWN,
+ OPTIONAL_CODECS_NOT_SUPPORTED,
+ OPTIONAL_CODECS_SUPPORTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OptionalCodecsSupportStatus {}
+
+ /**
+ * We don't have a stored preference for whether or not the given A2DP sink device supports
+ * optional codecs.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_SUPPORT_UNKNOWN = -1;
+
+ /**
+ * The given A2DP sink device does not support optional codecs.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_NOT_SUPPORTED = 0;
+
+ /**
+ * The given A2DP sink device does support optional codecs.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_SUPPORTED = 1;
+
+ /** @hide */
+ @IntDef(prefix = "OPTIONAL_CODECS_PREF_", value = {
+ OPTIONAL_CODECS_PREF_UNKNOWN,
+ OPTIONAL_CODECS_PREF_DISABLED,
+ OPTIONAL_CODECS_PREF_ENABLED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OptionalCodecsPreferenceStatus {}
+
+ /**
+ * We don't have a stored preference for whether optional codecs should be enabled or
+ * disabled for the given A2DP device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_PREF_UNKNOWN = -1;
+
+ /**
+ * Optional codecs should be disabled for the given A2DP device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_PREF_DISABLED = 0;
+
+ /**
+ * Optional codecs should be enabled for the given A2DP device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_PREF_ENABLED = 1;
+
+ /** @hide */
+ @IntDef(prefix = "DYNAMIC_BUFFER_SUPPORT_", value = {
+ DYNAMIC_BUFFER_SUPPORT_NONE,
+ DYNAMIC_BUFFER_SUPPORT_A2DP_OFFLOAD,
+ DYNAMIC_BUFFER_SUPPORT_A2DP_SOFTWARE_ENCODING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Type {}
+
+ /**
+ * Indicates the supported type of Dynamic Audio Buffer is not supported.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int DYNAMIC_BUFFER_SUPPORT_NONE = 0;
+
+ /**
+ * Indicates the supported type of Dynamic Audio Buffer is A2DP offload.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int DYNAMIC_BUFFER_SUPPORT_A2DP_OFFLOAD = 1;
+
+ /**
+ * Indicates the supported type of Dynamic Audio Buffer is A2DP software encoding.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int DYNAMIC_BUFFER_SUPPORT_A2DP_SOFTWARE_ENCODING = 2;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothA2dp> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.A2DP, "BluetoothA2dp",
+ IBluetoothA2dp.class.getName()) {
+ @Override
+ public IBluetoothA2dp getServiceInterface(IBinder service) {
+ return IBluetoothA2dp.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothA2dp proxy object for interacting with the local
+ * Bluetooth A2DP service.
+ */
+ /* package */ BluetoothA2dp(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @Override
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothA2dp getService() {
+ return mProfileConnector.getService();
+ }
+
+ @Override
+ public void finalize() {
+ // The empty finalize needs to be kept or the
+ // cts signature tests would fail.
+ }
+
+ /**
+ * Initiate connection to a profile of the remote Bluetooth device.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is already connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that
+ * connection state intent for the profile will be broadcasted with
+ * the state. Users can get the connection state of the profile
+ * from this intent.
+ *
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.connect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Initiate disconnection from a profile
+ *
+ * <p> This API will return false in scenarios like the profile on the
+ * Bluetooth device is not in connected state etc. When this API returns,
+ * true, it is guaranteed that the connection state change
+ * intent will be broadcasted with the state. Users can get the
+ * disconnection state of the profile from this intent.
+ *
+ * <p> If the disconnection is initiated by a remote device, the state
+ * will transition from {@link #STATE_CONNECTED} to
+ * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
+ * host (local) device the state will transition from
+ * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
+ * state {@link #STATE_DISCONNECTED}. The transition to
+ * {@link #STATE_DISCONNECTING} can be used to distinguish between the
+ * two scenarios.
+ *
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.disconnect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothA2dp service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothA2dp service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getDevicesMatchingConnectionStates(states,
+ mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @BtProfileState int getConnectionState(BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, A2DP audio streaming
+ * is to the active A2DP Sink device. If a remote device is not
+ * connected, it cannot be selected as active.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ * @param device the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage(trackingBug = 171933273)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && ((device == null) || isValidDevice(device))) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setActiveDevice(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the connected device that is active.
+ *
+ * @return the connected device that is active or null if no device
+ * is active
+ * @hide
+ */
+ @UnsupportedAppUsage(trackingBug = 171933273)
+ @Nullable
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothDevice getActiveDevice() {
+ if (VDBG) log("getActiveDevice()");
+ final IBluetoothA2dp service = getService();
+ final BluetoothDevice defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<BluetoothDevice> recv =
+ SynchronousResultReceiver.get();
+ service.getActiveDevice(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set priority of the profile
+ *
+ * <p> The device should already be paired.
+ * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF}
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ if (DBG) log("setPriority(" + device + ", " + priority + ")");
+ return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority));
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true if connectionPolicy is set, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothA2dp service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)
+ && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the priority of the profile.
+ *
+ * <p> The priority can be any of:
+ * {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public int getPriority(BluetoothDevice device) {
+ if (VDBG) log("getPriority(" + device + ")");
+ return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionPolicy(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Checks if Avrcp device supports the absolute volume feature.
+ *
+ * @return true if device supports absolute volume
+ * @hide
+ */
+ @RequiresNoPermission
+ public boolean isAvrcpAbsoluteVolumeSupported() {
+ if (DBG) Log.d(TAG, "isAvrcpAbsoluteVolumeSupported");
+ final IBluetoothA2dp service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.isAvrcpAbsoluteVolumeSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Tells remote device to set an absolute volume. Only if absolute volume is supported
+ *
+ * @param volume Absolute volume to be set on AVRCP side
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void setAvrcpAbsoluteVolume(int volume) {
+ if (DBG) Log.d(TAG, "setAvrcpAbsoluteVolume");
+ final IBluetoothA2dp service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.setAvrcpAbsoluteVolume(volume, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Check if A2DP profile is streaming music.
+ *
+ * @param device BluetoothDevice device
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isA2dpPlaying(BluetoothDevice device) {
+ if (DBG) log("isA2dpPlaying(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.isA2dpPlaying(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * This function checks if the remote device is an AVCRP
+ * target and thus whether we should send volume keys
+ * changes or not.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean shouldSendVolumeKeys(BluetoothDevice device) {
+ if (isEnabled() && isValidDevice(device)) {
+ ParcelUuid[] uuids = device.getUuids();
+ if (uuids == null) return false;
+
+ for (ParcelUuid uuid : uuids) {
+ if (uuid.equals(BluetoothUuid.AVRCP_TARGET)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the current codec status (configuration and capability).
+ *
+ * @param device the remote Bluetooth device.
+ * @return the current codec status
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public BluetoothCodecStatus getCodecStatus(@NonNull BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "getCodecStatus(" + device + ")");
+ verifyDeviceNotNull(device, "getCodecStatus");
+ final IBluetoothA2dp service = getService();
+ final BluetoothCodecStatus defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<BluetoothCodecStatus> recv =
+ SynchronousResultReceiver.get();
+ service.getCodecStatus(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the codec configuration preference.
+ *
+ * For apps without the {@link android.Manifest.permission.BLUETOOTH_PRIVILEGED} permission
+ * a {@link android.companion.CompanionDeviceManager} association is required.
+ *
+ * @param device the remote Bluetooth device.
+ * @param codecConfig the codec configuration preference
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void setCodecConfigPreference(@NonNull BluetoothDevice device,
+ @NonNull BluetoothCodecConfig codecConfig) {
+ if (DBG) Log.d(TAG, "setCodecConfigPreference(" + device + ")");
+ verifyDeviceNotNull(device, "setCodecConfigPreference");
+ if (codecConfig == null) {
+ Log.e(TAG, "setCodecConfigPreference: Codec config can't be null");
+ throw new IllegalArgumentException("codecConfig cannot be null");
+ }
+ final IBluetoothA2dp service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.setCodecConfigPreference(device, codecConfig, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Enables the optional codecs for the given device for this connection.
+ *
+ * If the given device supports another codec type than
+ * {@link BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC}, this will switch to it.
+ * Switching from one codec to another will create a short audio drop.
+ * In case of multiple applications calling the method, the last call will be taken into
+ * account, overriding any previous call
+ *
+ * See {@link #setOptionalCodecsEnabled} to enable optional codecs by default
+ * when the given device is connected.
+ *
+ * @param device the remote Bluetooth device
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void enableOptionalCodecs(@NonNull BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "enableOptionalCodecs(" + device + ")");
+ verifyDeviceNotNull(device, "enableOptionalCodecs");
+ enableDisableOptionalCodecs(device, true);
+ }
+
+ /**
+ * Disables the optional codecs for the given device for this connection.
+ *
+ * When optional codecs are disabled, the device will use the default
+ * Bluetooth audio codec type.
+ * Switching from one codec to another will create a short audio drop.
+ * In case of multiple applications calling the method, the last call will be taken into
+ * account, overriding any previous call
+ *
+ * See {@link BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC}.
+ * See {@link #setOptionalCodecsEnabled} to disable optional codecs by default
+ * when the given device is connected.
+ *
+ * @param device the remote Bluetooth device
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void disableOptionalCodecs(@NonNull BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "disableOptionalCodecs(" + device + ")");
+ verifyDeviceNotNull(device, "disableOptionalCodecs");
+ enableDisableOptionalCodecs(device, false);
+ }
+
+ /**
+ * Enables or disables the optional codecs.
+ *
+ * @param device the remote Bluetooth device.
+ * @param enable if true, enable the optional codecs, otherwise disable them
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ private void enableDisableOptionalCodecs(BluetoothDevice device, boolean enable) {
+ final IBluetoothA2dp service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ if (enable) {
+ service.enableOptionalCodecs(device, mAttributionSource);
+ } else {
+ service.disableOptionalCodecs(device, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Returns whether this device supports optional codecs.
+ *
+ * @param device the remote Bluetooth device
+ * @return whether the optional codecs are supported or not, or
+ * {@link #OPTIONAL_CODECS_SUPPORT_UNKNOWN} if the state
+ * can't be retrieved.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @OptionalCodecsSupportStatus
+ public int isOptionalCodecsSupported(
+ @NonNull BluetoothDevice device) {
+ if (DBG) log("isOptionalCodecsSupported(" + device + ")");
+ verifyDeviceNotNull(device, "isOptionalCodecsSupported");
+ final IBluetoothA2dp service = getService();
+ final int defaultValue = OPTIONAL_CODECS_SUPPORT_UNKNOWN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.isOptionalCodecsSupported(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns whether this device has its optional codecs enabled.
+ *
+ * @param device the remote Bluetooth device
+ * @return whether the optional codecs are enabled or not, or
+ * {@link #OPTIONAL_CODECS_PREF_UNKNOWN} if the state
+ * can't be retrieved.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @OptionalCodecsPreferenceStatus
+ public int isOptionalCodecsEnabled(
+ @NonNull BluetoothDevice device) {
+ if (DBG) log("isOptionalCodecsEnabled(" + device + ")");
+ verifyDeviceNotNull(device, "isOptionalCodecsEnabled");
+ final IBluetoothA2dp service = getService();
+ final int defaultValue = OPTIONAL_CODECS_PREF_UNKNOWN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.isOptionalCodecsEnabled(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the default state of optional codecs for the given device.
+ *
+ * Automatically enables or disables the optional codecs for the given device when
+ * connected.
+ *
+ * @param device the remote Bluetooth device
+ * @param value whether the optional codecs should be enabled for this device
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void setOptionalCodecsEnabled(@NonNull BluetoothDevice device,
+ @OptionalCodecsPreferenceStatus int value) {
+ if (DBG) log("setOptionalCodecsEnabled(" + device + ")");
+ verifyDeviceNotNull(device, "setOptionalCodecsEnabled");
+ if (value != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN
+ && value != BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED
+ && value != BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) {
+ Log.e(TAG, "Invalid value passed to setOptionalCodecsEnabled: " + value);
+ return;
+ }
+ final IBluetoothA2dp service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ service.setOptionalCodecsEnabled(device, value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Get the supported type of the Dynamic Audio Buffer.
+ * <p>Possible return values are
+ * {@link #DYNAMIC_BUFFER_SUPPORT_NONE},
+ * {@link #DYNAMIC_BUFFER_SUPPORT_A2DP_OFFLOAD},
+ * {@link #DYNAMIC_BUFFER_SUPPORT_A2DP_SOFTWARE_ENCODING}.
+ *
+ * @return supported type of Dynamic Audio Buffer feature
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Type int getDynamicBufferSupport() {
+ if (VDBG) log("getDynamicBufferSupport()");
+ final IBluetoothA2dp service = getService();
+ final int defaultValue = DYNAMIC_BUFFER_SUPPORT_NONE;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getDynamicBufferSupport(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Return the record of {@link BufferConstraints} object that
+ * has the default/maximum/minimum audio buffer. This can be used to inform what the controller
+ * has support for the audio buffer.
+ *
+ * @return a record with {@link BufferConstraints} or null if report is unavailable
+ * or unsupported
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Nullable BufferConstraints getBufferConstraints() {
+ if (VDBG) log("getBufferConstraints()");
+ final IBluetoothA2dp service = getService();
+ final BufferConstraints defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<BufferConstraints> recv =
+ SynchronousResultReceiver.get();
+ service.getBufferConstraints(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set Dynamic Audio Buffer Size.
+ *
+ * @param codec audio codec
+ * @param value buffer millis
+ * @return true to indicate success, or false on immediate error
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setBufferLengthMillis(@BluetoothCodecConfig.SourceCodecType int codec,
+ int value) {
+ if (VDBG) log("setBufferLengthMillis(" + codec + ", " + value + ")");
+ if (value < 0) {
+ Log.e(TAG, "Trying to set audio buffer length to a negative value: " + value);
+ return false;
+ }
+ final IBluetoothA2dp service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setBufferLengthMillis(codec, value, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Helper for converting a state to a string.
+ *
+ * For debug use only - strings are not internationalized.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_DISCONNECTED:
+ return "disconnected";
+ case STATE_CONNECTING:
+ return "connecting";
+ case STATE_CONNECTED:
+ return "connected";
+ case STATE_DISCONNECTING:
+ return "disconnecting";
+ case STATE_PLAYING:
+ return "playing";
+ case STATE_NOT_PLAYING:
+ return "not playing";
+ default:
+ return "<unknown state " + state + ">";
+ }
+ }
+
+ private boolean isEnabled() {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+ return false;
+ }
+
+ private void verifyDeviceNotNull(BluetoothDevice device, String methodName) {
+ if (device == null) {
+ Log.e(TAG, methodName + ": device param is null");
+ throw new IllegalArgumentException("Device cannot be null");
+ }
+ }
+
+ private boolean isValidDevice(BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothA2dpSink.java b/android-34/android/bluetooth/BluetoothA2dpSink.java
new file mode 100644
index 0000000..c299b17
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothA2dpSink.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Bluetooth A2DP Sink
+ * profile.
+ *
+ * <p>BluetoothA2dpSink is a proxy object for controlling the Bluetooth A2DP Sink
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothA2dpSink proxy object.
+ *
+ * @hide
+ */
+@SystemApi
+public final class BluetoothA2dpSink implements BluetoothProfile {
+ private static final String TAG = "BluetoothA2dpSink";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the A2DP Sink
+ * profile.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED";
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothA2dpSink> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.A2DP_SINK,
+ "BluetoothA2dpSink", IBluetoothA2dpSink.class.getName()) {
+ @Override
+ public IBluetoothA2dpSink getServiceInterface(IBinder service) {
+ return IBluetoothA2dpSink.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothA2dp proxy object for interacting with the local
+ * Bluetooth A2DP service.
+ */
+ /* package */ BluetoothA2dpSink(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothA2dpSink getService() {
+ return mProfileConnector.getService();
+ }
+
+ @Override
+ public void finalize() {
+ close();
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <p> Currently, the system supports only 1 connection to the
+ * A2DP profile. The API will automatically disconnect connected
+ * devices before connecting.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is already connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that
+ * connection state intent for the profile will be broadcasted with
+ * the state. Users can get the connection state of the profile
+ * from this intent.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.connect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Initiate disconnection from a profile
+ *
+ * <p> This API will return false in scenarios like the profile on the
+ * Bluetooth device is not in connected state etc. When this API returns,
+ * true, it is guaranteed that the connection state change
+ * intent will be broadcasted with the state. Users can get the
+ * disconnection state of the profile from this intent.
+ *
+ * <p> If the disconnection is initiated by a remote device, the state
+ * will transition from {@link #STATE_CONNECTED} to
+ * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
+ * host (local) device the state will transition from
+ * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
+ * state {@link #STATE_DISCONNECTED}. The transition to
+ * {@link #STATE_DISCONNECTING} can be used to distinguish between the
+ * two scenarios.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.disconnect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothA2dpSink service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothA2dpSink service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (VDBG) log("getConnectionState(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the current audio configuration for the A2DP source device,
+ * or null if the device has no audio configuration
+ *
+ * @param device Remote bluetooth device.
+ * @return audio configuration for the device, or null
+ *
+ * {@see BluetoothAudioConfig}
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
+ if (VDBG) log("getAudioConfig(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ final BluetoothAudioConfig defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<BluetoothAudioConfig> recv =
+ SynchronousResultReceiver.get();
+ service.getAudioConfig(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set priority of the profile
+ *
+ * <p> The device should already be paired.
+ * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF}
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ if (DBG) log("setPriority(" + device + ", " + priority + ")");
+ return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority));
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true if connectionPolicy is set, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothA2dpSink service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)
+ && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the priority of the profile.
+ *
+ * <p> The priority can be any of:
+ * {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public int getPriority(BluetoothDevice device) {
+ if (VDBG) log("getPriority(" + device + ")");
+ return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionPolicy(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Check if audio is playing on the bluetooth device (A2DP profile is streaming music).
+ *
+ * @param device BluetoothDevice device
+ * @return true if audio is playing (A2dp is streaming music), false otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean isAudioPlaying(@NonNull BluetoothDevice device) {
+ if (VDBG) log("isAudioPlaying(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.isA2dpPlaying(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Helper for converting a state to a string.
+ *
+ * For debug use only - strings are not internationalized.
+ *
+ * @hide
+ */
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_DISCONNECTED:
+ return "disconnected";
+ case STATE_CONNECTING:
+ return "connecting";
+ case STATE_CONNECTED:
+ return "connected";
+ case STATE_DISCONNECTING:
+ return "disconnecting";
+ case BluetoothA2dp.STATE_PLAYING:
+ return "playing";
+ case BluetoothA2dp.STATE_NOT_PLAYING:
+ return "not playing";
+ default:
+ return "<unknown state " + state + ">";
+ }
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothActivityEnergyInfo.java b/android-34/android/bluetooth/BluetoothActivityEnergyInfo.java
new file mode 100644
index 0000000..c17a7b4
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothActivityEnergyInfo.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Record of energy and activity information from controller and
+ * underlying bt stack state.Timestamp the record with system
+ * time.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+public final class BluetoothActivityEnergyInfo implements Parcelable {
+ private final long mTimestamp;
+ private int mBluetoothStackState;
+ private long mControllerTxTimeMs;
+ private long mControllerRxTimeMs;
+ private long mControllerIdleTimeMs;
+ private long mControllerEnergyUsed;
+ private List<UidTraffic> mUidTraffic;
+
+ /** @hide */
+ @IntDef(prefix = { "BT_STACK_STATE_" }, value = {
+ BT_STACK_STATE_INVALID,
+ BT_STACK_STATE_STATE_ACTIVE,
+ BT_STACK_STATE_STATE_SCANNING,
+ BT_STACK_STATE_STATE_IDLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BluetoothStackState {}
+
+ public static final int BT_STACK_STATE_INVALID = 0;
+ public static final int BT_STACK_STATE_STATE_ACTIVE = 1;
+ public static final int BT_STACK_STATE_STATE_SCANNING = 2;
+ public static final int BT_STACK_STATE_STATE_IDLE = 3;
+
+ /** @hide */
+ public BluetoothActivityEnergyInfo(long timestamp, int stackState,
+ long txTime, long rxTime, long idleTime, long energyUsed) {
+ mTimestamp = timestamp;
+ mBluetoothStackState = stackState;
+ mControllerTxTimeMs = txTime;
+ mControllerRxTimeMs = rxTime;
+ mControllerIdleTimeMs = idleTime;
+ mControllerEnergyUsed = energyUsed;
+ }
+
+ /** @hide */
+ private BluetoothActivityEnergyInfo(Parcel in) {
+ mTimestamp = in.readLong();
+ mBluetoothStackState = in.readInt();
+ mControllerTxTimeMs = in.readLong();
+ mControllerRxTimeMs = in.readLong();
+ mControllerIdleTimeMs = in.readLong();
+ mControllerEnergyUsed = in.readLong();
+ mUidTraffic = in.createTypedArrayList(UidTraffic.CREATOR);
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return "BluetoothActivityEnergyInfo{"
+ + " mTimestamp=" + mTimestamp
+ + " mBluetoothStackState=" + mBluetoothStackState
+ + " mControllerTxTimeMs=" + mControllerTxTimeMs
+ + " mControllerRxTimeMs=" + mControllerRxTimeMs
+ + " mControllerIdleTimeMs=" + mControllerIdleTimeMs
+ + " mControllerEnergyUsed=" + mControllerEnergyUsed
+ + " mUidTraffic=" + mUidTraffic
+ + " }";
+ }
+
+ public static final @NonNull Parcelable.Creator<BluetoothActivityEnergyInfo> CREATOR =
+ new Parcelable.Creator<BluetoothActivityEnergyInfo>() {
+ public BluetoothActivityEnergyInfo createFromParcel(Parcel in) {
+ return new BluetoothActivityEnergyInfo(in);
+ }
+
+ public BluetoothActivityEnergyInfo[] newArray(int size) {
+ return new BluetoothActivityEnergyInfo[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mTimestamp);
+ out.writeInt(mBluetoothStackState);
+ out.writeLong(mControllerTxTimeMs);
+ out.writeLong(mControllerRxTimeMs);
+ out.writeLong(mControllerIdleTimeMs);
+ out.writeLong(mControllerEnergyUsed);
+ out.writeTypedList(mUidTraffic);
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Get the Bluetooth stack state associated with the energy info.
+ *
+ * @return one of {@link #BluetoothStackState} states
+ */
+ @BluetoothStackState
+ public int getBluetoothStackState() {
+ return mBluetoothStackState;
+ }
+
+ /**
+ * @return tx time in ms
+ */
+ public long getControllerTxTimeMillis() {
+ return mControllerTxTimeMs;
+ }
+
+ /**
+ * @return rx time in ms
+ */
+ public long getControllerRxTimeMillis() {
+ return mControllerRxTimeMs;
+ }
+
+ /**
+ * @return idle time in ms
+ */
+ public long getControllerIdleTimeMillis() {
+ return mControllerIdleTimeMs;
+ }
+
+ /**
+ * Get the product of current (mA), voltage (V), and time (ms).
+ *
+ * @return energy used
+ */
+ public long getControllerEnergyUsed() {
+ return mControllerEnergyUsed;
+ }
+
+ /**
+ * @return timestamp (real time elapsed in milliseconds since boot) of record creation
+ */
+ public @ElapsedRealtimeLong long getTimestampMillis() {
+ return mTimestamp;
+ }
+
+ /**
+ * Get the {@link List} of each application {@link android.bluetooth.UidTraffic}.
+ *
+ * @return current {@link List} of {@link android.bluetooth.UidTraffic}
+ */
+ public @NonNull List<UidTraffic> getUidTraffic() {
+ if (mUidTraffic == null) {
+ return Collections.emptyList();
+ }
+ return mUidTraffic;
+ }
+
+ /** @hide */
+ public void setUidTraffic(List<UidTraffic> traffic) {
+ mUidTraffic = traffic;
+ }
+
+ /**
+ * @return true if the record Tx time, Rx time, and Idle time are more than 0.
+ */
+ public boolean isValid() {
+ return ((mControllerTxTimeMs >= 0) && (mControllerRxTimeMs >= 0)
+ && (mControllerIdleTimeMs >= 0));
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothAdapter.java b/android-34/android/bluetooth/BluetoothAdapter.java
new file mode 100644
index 0000000..6a27b74
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothAdapter.java
@@ -0,0 +1,5737 @@
+/*
+ * Copyright 2009-2016 The Android Open Source Project
+ * Copyright 2015 Samsung LSI
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothDevice.AddressType;
+import android.bluetooth.BluetoothDevice.Transport;
+import android.bluetooth.BluetoothProfile.ConnectionPolicy;
+import android.bluetooth.annotations.RequiresBluetoothAdvertisePermission;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresBluetoothLocationPermission;
+import android.bluetooth.annotations.RequiresBluetoothScanPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.DistanceMeasurementManager;
+import android.bluetooth.le.PeriodicAdvertisingManager;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Binder;
+import android.os.BluetoothServiceManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.IpcDataCache;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.sysprop.BluetoothProperties;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Represents the local device Bluetooth adapter. The {@link BluetoothAdapter}
+ * lets you perform fundamental Bluetooth tasks, such as initiate
+ * device discovery, query a list of bonded (paired) devices,
+ * instantiate a {@link BluetoothDevice} using a known MAC address, and create
+ * a {@link BluetoothServerSocket} to listen for connection requests from other
+ * devices, and start a scan for Bluetooth LE devices.
+ *
+ * <p>To get a {@link BluetoothAdapter} representing the local Bluetooth
+ * adapter, call the {@link BluetoothManager#getAdapter} function on {@link BluetoothManager}.
+ * On JELLY_BEAN_MR1 and below you will need to use the static {@link #getDefaultAdapter}
+ * method instead.
+ * </p><p>
+ * Fundamentally, this is your starting point for all
+ * Bluetooth actions. Once you have the local adapter, you can get a set of
+ * {@link BluetoothDevice} objects representing all paired devices with
+ * {@link #getBondedDevices()}; start device discovery with
+ * {@link #startDiscovery()}; or create a {@link BluetoothServerSocket} to
+ * listen for incoming RFComm connection requests with {@link
+ * #listenUsingRfcommWithServiceRecord(String, UUID)}; listen for incoming L2CAP Connection-oriented
+ * Channels (CoC) connection requests with {@link #listenUsingL2capChannel()}; or start a scan for
+ * Bluetooth LE devices with {@link #startLeScan(LeScanCallback callback)}.
+ * </p>
+ * <p>This class is thread safe.</p>
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>
+ * For more information about using Bluetooth, read the <a href=
+ * "{@docRoot}guide/topics/connectivity/bluetooth.html">Bluetooth</a> developer
+ * guide.
+ * </p>
+ * </div>
+ *
+ * {@see BluetoothDevice}
+ * {@see BluetoothServerSocket}
+ */
+public final class BluetoothAdapter {
+ private static final String TAG = "BluetoothAdapter";
+ private static final String DESCRIPTOR = "android.bluetooth.BluetoothAdapter";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Default MAC address reported to a client that does not have the
+ * {@link android.Manifest.permission#LOCAL_MAC_ADDRESS} permission.
+ *
+ *
+ * @hide
+ */
+ public static final String DEFAULT_MAC_ADDRESS = "02:00:00:00:00:00";
+
+ /**
+ * Sentinel error value for this class. Guaranteed to not equal any other
+ * integer constant in this class. Provided as a convenience for functions
+ * that require a sentinel error value, for example:
+ * <p><code>Intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ * BluetoothAdapter.ERROR)</code>
+ */
+ public static final int ERROR = Integer.MIN_VALUE;
+
+ /**
+ * Broadcast Action: The state of the local Bluetooth adapter has been
+ * changed.
+ * <p>For example, Bluetooth has been turned on or off.
+ * <p>Always contains the extra fields {@link #EXTRA_STATE} and {@link
+ * #EXTRA_PREVIOUS_STATE} containing the new and old states
+ * respectively.
+ */
+ @RequiresLegacyBluetoothPermission
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_STATE_CHANGED = "android.bluetooth.adapter.action.STATE_CHANGED";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_STATE_CHANGED}
+ * intents to request the current power state. Possible values are:
+ * {@link #STATE_OFF},
+ * {@link #STATE_TURNING_ON},
+ * {@link #STATE_ON},
+ * {@link #STATE_TURNING_OFF},
+ */
+ public static final String EXTRA_STATE = "android.bluetooth.adapter.extra.STATE";
+ /**
+ * Used as an int extra field in {@link #ACTION_STATE_CHANGED}
+ * intents to request the previous power state. Possible values are:
+ * {@link #STATE_OFF},
+ * {@link #STATE_TURNING_ON},
+ * {@link #STATE_ON},
+ * {@link #STATE_TURNING_OFF}
+ */
+ public static final String EXTRA_PREVIOUS_STATE =
+ "android.bluetooth.adapter.extra.PREVIOUS_STATE";
+
+ /** @hide */
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_OFF,
+ STATE_TURNING_ON,
+ STATE_ON,
+ STATE_TURNING_OFF,
+ STATE_BLE_TURNING_ON,
+ STATE_BLE_ON,
+ STATE_BLE_TURNING_OFF
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface InternalAdapterState {}
+
+ /** @hide */
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_OFF,
+ STATE_TURNING_ON,
+ STATE_ON,
+ STATE_TURNING_OFF,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AdapterState {}
+
+ /**
+ * Indicates the local Bluetooth adapter is off.
+ */
+ public static final int STATE_OFF = 10;
+ /**
+ * Indicates the local Bluetooth adapter is turning on. However local
+ * clients should wait for {@link #STATE_ON} before attempting to
+ * use the adapter.
+ */
+ public static final int STATE_TURNING_ON = 11;
+ /**
+ * Indicates the local Bluetooth adapter is on, and ready for use.
+ */
+ public static final int STATE_ON = 12;
+ /**
+ * Indicates the local Bluetooth adapter is turning off. Local clients
+ * should immediately attempt graceful disconnection of any remote links.
+ */
+ public static final int STATE_TURNING_OFF = 13;
+
+ /**
+ * Indicates the local Bluetooth adapter is turning Bluetooth LE mode on.
+ *
+ * @hide
+ */
+ public static final int STATE_BLE_TURNING_ON = 14;
+
+ /**
+ * Indicates the local Bluetooth adapter is in LE only mode.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int STATE_BLE_ON = 15;
+
+ /**
+ * Indicates the local Bluetooth adapter is turning off LE only mode.
+ *
+ * @hide
+ */
+ public static final int STATE_BLE_TURNING_OFF = 16;
+
+ /**
+ * UUID of the GATT Read Characteristics for LE_PSM value.
+ *
+ * @hide
+ */
+ public static final UUID LE_PSM_CHARACTERISTIC_UUID =
+ UUID.fromString("2d410339-82b6-42aa-b34e-e2e01df8cc1a");
+
+ /**
+ * Used as an optional extra field for the {@link PendingIntent} provided to {@link
+ * #startRfcommServer(String, UUID, PendingIntent)}. This is useful for when an
+ * application registers multiple RFCOMM listeners, and needs a way to determine which service
+ * record the incoming {@link BluetoothSocket} is using.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_RFCOMM_LISTENER_ID =
+ "android.bluetooth.adapter.extra.RFCOMM_LISTENER_ID";
+
+ /** @hide */
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_TIMEOUT,
+ BluetoothStatusCodes.RFCOMM_LISTENER_START_FAILED_UUID_IN_USE,
+ BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD,
+ BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_DIFFERENT_APP,
+ BluetoothStatusCodes.RFCOMM_LISTENER_FAILED_TO_CREATE_SERVER_SOCKET,
+ BluetoothStatusCodes.RFCOMM_LISTENER_FAILED_TO_CLOSE_SERVER_SOCKET,
+ BluetoothStatusCodes.RFCOMM_LISTENER_NO_SOCKET_AVAILABLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RfcommListenerResult {}
+
+ /**
+ * Human-readable string helper for AdapterState and InternalAdapterState
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresNoPermission
+ @NonNull
+ public static String nameForState(@InternalAdapterState int state) {
+ switch (state) {
+ case STATE_OFF:
+ return "OFF";
+ case STATE_TURNING_ON:
+ return "TURNING_ON";
+ case STATE_ON:
+ return "ON";
+ case STATE_TURNING_OFF:
+ return "TURNING_OFF";
+ case STATE_BLE_TURNING_ON:
+ return "BLE_TURNING_ON";
+ case STATE_BLE_ON:
+ return "BLE_ON";
+ case STATE_BLE_TURNING_OFF:
+ return "BLE_TURNING_OFF";
+ default:
+ return "?!?!? (" + state + ")";
+ }
+ }
+
+ /**
+ * Activity Action: Show a system activity that requests discoverable mode.
+ * This activity will also request the user to turn on Bluetooth if it
+ * is not currently enabled.
+ * <p>Discoverable mode is equivalent to {@link
+ * #SCAN_MODE_CONNECTABLE_DISCOVERABLE}. It allows remote devices to see
+ * this Bluetooth adapter when they perform a discovery.
+ * <p>For privacy, Android is not discoverable by default.
+ * <p>The sender of this Intent can optionally use extra field {@link
+ * #EXTRA_DISCOVERABLE_DURATION} to request the duration of
+ * discoverability. Currently the default duration is 120 seconds, and
+ * maximum duration is capped at 300 seconds for each request.
+ * <p>Notification of the result of this activity is posted using the
+ * {@link android.app.Activity#onActivityResult} callback. The
+ * <code>resultCode</code>
+ * will be the duration (in seconds) of discoverability or
+ * {@link android.app.Activity#RESULT_CANCELED} if the user rejected
+ * discoverability or an error has occurred.
+ * <p>Applications can also listen for {@link #ACTION_SCAN_MODE_CHANGED}
+ * for global notification whenever the scan mode changes. For example, an
+ * application can be notified when the device has ended discoverability.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String
+ ACTION_REQUEST_DISCOVERABLE = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";
+
+ /**
+ * Used as an optional int extra field in {@link
+ * #ACTION_REQUEST_DISCOVERABLE} intents to request a specific duration
+ * for discoverability in seconds. The current default is 120 seconds, and
+ * requests over 300 seconds will be capped. These values could change.
+ */
+ public static final String EXTRA_DISCOVERABLE_DURATION =
+ "android.bluetooth.adapter.extra.DISCOVERABLE_DURATION";
+
+ /**
+ * Activity Action: Show a system activity that allows the user to turn on
+ * Bluetooth.
+ * <p>This system activity will return once Bluetooth has completed turning
+ * on, or the user has decided not to turn Bluetooth on.
+ * <p>Notification of the result of this activity is posted using the
+ * {@link android.app.Activity#onActivityResult} callback. The
+ * <code>resultCode</code>
+ * will be {@link android.app.Activity#RESULT_OK} if Bluetooth has been
+ * turned on or {@link android.app.Activity#RESULT_CANCELED} if the user
+ * has rejected the request or an error has occurred.
+ * <p>Applications can also listen for {@link #ACTION_STATE_CHANGED}
+ * for global notification whenever Bluetooth is turned on or off.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String
+ ACTION_REQUEST_ENABLE = "android.bluetooth.adapter.action.REQUEST_ENABLE";
+
+ /**
+ * Activity Action: Show a system activity that allows the user to turn off
+ * Bluetooth. This is used only if permission review is enabled which is for
+ * apps targeting API less than 23 require a permission review before any of
+ * the app's components can run.
+ * <p>This system activity will return once Bluetooth has completed turning
+ * off, or the user has decided not to turn Bluetooth off.
+ * <p>Notification of the result of this activity is posted using the
+ * {@link android.app.Activity#onActivityResult} callback. The
+ * <code>resultCode</code>
+ * will be {@link android.app.Activity#RESULT_OK} if Bluetooth has been
+ * turned off or {@link android.app.Activity#RESULT_CANCELED} if the user
+ * has rejected the request or an error has occurred.
+ * <p>Applications can also listen for {@link #ACTION_STATE_CHANGED}
+ * for global notification whenever Bluetooth is turned on or off.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String
+ ACTION_REQUEST_DISABLE = "android.bluetooth.adapter.action.REQUEST_DISABLE";
+
+ /**
+ * Activity Action: Show a system activity that allows user to enable BLE scans even when
+ * Bluetooth is turned off.<p>
+ *
+ * Notification of result of this activity is posted using
+ * {@link android.app.Activity#onActivityResult}. The <code>resultCode</code> will be
+ * {@link android.app.Activity#RESULT_OK} if BLE scan always available setting is turned on or
+ * {@link android.app.Activity#RESULT_CANCELED} if the user has rejected the request or an
+ * error occurred.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_REQUEST_BLE_SCAN_ALWAYS_AVAILABLE =
+ "android.bluetooth.adapter.action.REQUEST_BLE_SCAN_ALWAYS_AVAILABLE";
+
+ /**
+ * Broadcast Action: Indicates the Bluetooth scan mode of the local Adapter
+ * has changed.
+ * <p>Always contains the extra fields {@link #EXTRA_SCAN_MODE} and {@link
+ * #EXTRA_PREVIOUS_SCAN_MODE} containing the new and old scan modes
+ * respectively.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_SCAN_MODE_CHANGED = "android.bluetooth.adapter.action.SCAN_MODE_CHANGED";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_SCAN_MODE_CHANGED}
+ * intents to request the current scan mode. Possible values are:
+ * {@link #SCAN_MODE_NONE},
+ * {@link #SCAN_MODE_CONNECTABLE},
+ * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE},
+ */
+ public static final String EXTRA_SCAN_MODE = "android.bluetooth.adapter.extra.SCAN_MODE";
+ /**
+ * Used as an int extra field in {@link #ACTION_SCAN_MODE_CHANGED}
+ * intents to request the previous scan mode. Possible values are:
+ * {@link #SCAN_MODE_NONE},
+ * {@link #SCAN_MODE_CONNECTABLE},
+ * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE},
+ */
+ public static final String EXTRA_PREVIOUS_SCAN_MODE =
+ "android.bluetooth.adapter.extra.PREVIOUS_SCAN_MODE";
+
+ /** @hide */
+ @IntDef(prefix = { "SCAN_" }, value = {
+ SCAN_MODE_NONE,
+ SCAN_MODE_CONNECTABLE,
+ SCAN_MODE_CONNECTABLE_DISCOVERABLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScanMode {}
+
+ /** @hide */
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_SCAN_PERMISSION
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScanModeStatusCode {}
+
+ /**
+ * Indicates that both inquiry scan and page scan are disabled on the local
+ * Bluetooth adapter. Therefore this device is neither discoverable
+ * nor connectable from remote Bluetooth devices.
+ */
+ public static final int SCAN_MODE_NONE = 20;
+ /**
+ * Indicates that inquiry scan is disabled, but page scan is enabled on the
+ * local Bluetooth adapter. Therefore this device is not discoverable from
+ * remote Bluetooth devices, but is connectable from remote devices that
+ * have previously discovered this device.
+ */
+ public static final int SCAN_MODE_CONNECTABLE = 21;
+ /**
+ * Indicates that both inquiry scan and page scan are enabled on the local
+ * Bluetooth adapter. Therefore this device is both discoverable and
+ * connectable from remote Bluetooth devices.
+ */
+ public static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE = 23;
+
+
+ /**
+ * Used as parameter for {@link #setBluetoothHciSnoopLoggingMode}, indicates that
+ * the Bluetooth HCI snoop logging should be disabled.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int BT_SNOOP_LOG_MODE_DISABLED = 0;
+
+ /**
+ * Used as parameter for {@link #setBluetoothHciSnoopLoggingMode}, indicates that
+ * the Bluetooth HCI snoop logging should be enabled without collecting potential
+ * Personally Identifiable Information and packet data.
+ *
+ * See {@link #BT_SNOOP_LOG_MODE_FULL} to enable logging of all information available.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int BT_SNOOP_LOG_MODE_FILTERED = 1;
+
+ /**
+ * Used as parameter for {@link #setSnoopLogMode}, indicates that the Bluetooth HCI snoop
+ * logging should be enabled.
+ *
+ * See {@link #BT_SNOOP_LOG_MODE_FILTERED} to enable logging with filtered information.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int BT_SNOOP_LOG_MODE_FULL = 2;
+
+ /** @hide */
+ @IntDef(value = {
+ BT_SNOOP_LOG_MODE_DISABLED,
+ BT_SNOOP_LOG_MODE_FILTERED,
+ BT_SNOOP_LOG_MODE_FULL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BluetoothSnoopLogMode {}
+
+ /** @hide */
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SetSnoopLogModeStatusCode {}
+
+ /**
+ * Device only has a display.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_OUT = 0;
+
+ /**
+ * Device has a display and the ability to input Yes/No.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_IO = 1;
+
+ /**
+ * Device only has a keyboard for entry but no display.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_IN = 2;
+
+ /**
+ * Device has no Input or Output capability.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_NONE = 3;
+
+ /**
+ * Device has a display and a full keyboard.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_KBDISP = 4;
+
+ /**
+ * Maximum range value for Input/Output capabilities.
+ *
+ * <p>This should be updated when adding a new Input/Output capability. Other code
+ * like validation depends on this being accurate.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_MAX = 5;
+
+ /**
+ * The Input/Output capability of the device is unknown.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_UNKNOWN = 255;
+
+ /** @hide */
+ @IntDef({IO_CAPABILITY_OUT, IO_CAPABILITY_IO, IO_CAPABILITY_IN, IO_CAPABILITY_NONE,
+ IO_CAPABILITY_KBDISP})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface IoCapability {}
+
+ /** @hide */
+ @IntDef(prefix = "ACTIVE_DEVICE_", value = {ACTIVE_DEVICE_AUDIO,
+ ACTIVE_DEVICE_PHONE_CALL, ACTIVE_DEVICE_ALL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActiveDeviceUse {}
+
+ /**
+ * Use the specified device for audio (a2dp and hearing aid profile)
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACTIVE_DEVICE_AUDIO = 0;
+
+ /**
+ * Use the specified device for phone calls (headset profile and hearing
+ * aid profile)
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACTIVE_DEVICE_PHONE_CALL = 1;
+
+ /**
+ * Use the specified device for a2dp, hearing aid profile, and headset profile
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACTIVE_DEVICE_ALL = 2;
+
+ /** @hide */
+ @IntDef({BluetoothProfile.HEADSET, BluetoothProfile.A2DP,
+ BluetoothProfile.HEARING_AID})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActiveDeviceProfile {}
+
+ /**
+ * Broadcast Action: The local Bluetooth adapter has started the remote
+ * device discovery process.
+ * <p>This usually involves an inquiry scan of about 12 seconds, followed
+ * by a page scan of each new device to retrieve its Bluetooth name.
+ * <p>Register for {@link BluetoothDevice#ACTION_FOUND} to be notified as
+ * remote Bluetooth devices are found.
+ * <p>Device discovery is a heavyweight procedure. New connections to
+ * remote Bluetooth devices should not be attempted while discovery is in
+ * progress, and existing connections will experience limited bandwidth
+ * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing
+ * discovery.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_DISCOVERY_STARTED = "android.bluetooth.adapter.action.DISCOVERY_STARTED";
+ /**
+ * Broadcast Action: The local Bluetooth adapter has finished the device
+ * discovery process.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_DISCOVERY_FINISHED = "android.bluetooth.adapter.action.DISCOVERY_FINISHED";
+
+ /**
+ * Broadcast Action: The local Bluetooth adapter has changed its friendly
+ * Bluetooth name.
+ * <p>This name is visible to remote Bluetooth devices.
+ * <p>Always contains the extra field {@link #EXTRA_LOCAL_NAME} containing
+ * the name.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_LOCAL_NAME_CHANGED = "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED";
+ /**
+ * Used as a String extra field in {@link #ACTION_LOCAL_NAME_CHANGED}
+ * intents to request the local Bluetooth name.
+ */
+ public static final String EXTRA_LOCAL_NAME = "android.bluetooth.adapter.extra.LOCAL_NAME";
+
+ /**
+ * Intent used to broadcast the change in connection state of the local
+ * Bluetooth adapter to a profile of the remote device. When the adapter is
+ * not connected to any profiles of any remote devices and it attempts a
+ * connection to a profile this intent will be sent. Once connected, this intent
+ * will not be sent for any more connection attempts to any profiles of any
+ * remote device. When the adapter disconnects from the last profile its
+ * connected to of any remote device, this intent will be sent.
+ *
+ * <p> This intent is useful for applications that are only concerned about
+ * whether the local adapter is connected to any profile of any device and
+ * are not really concerned about which profile. For example, an application
+ * which displays an icon to display whether Bluetooth is connected or not
+ * can use this intent.
+ *
+ * <p>This intent will have 3 extras:
+ * {@link #EXTRA_CONNECTION_STATE} - The current connection state.
+ * {@link #EXTRA_PREVIOUS_CONNECTION_STATE}- The previous connection state.
+ * {@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
+ *
+ * {@link #EXTRA_CONNECTION_STATE} or {@link #EXTRA_PREVIOUS_CONNECTION_STATE}
+ * can be any of {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Extra used by {@link #ACTION_CONNECTION_STATE_CHANGED}
+ *
+ * This extra represents the current connection state.
+ */
+ public static final String EXTRA_CONNECTION_STATE =
+ "android.bluetooth.adapter.extra.CONNECTION_STATE";
+
+ /**
+ * Extra used by {@link #ACTION_CONNECTION_STATE_CHANGED}
+ *
+ * This extra represents the previous connection state.
+ */
+ public static final String EXTRA_PREVIOUS_CONNECTION_STATE =
+ "android.bluetooth.adapter.extra.PREVIOUS_CONNECTION_STATE";
+
+ /**
+ * Broadcast Action: The Bluetooth adapter state has changed in LE only mode.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SystemApi public static final String ACTION_BLE_STATE_CHANGED =
+ "android.bluetooth.adapter.action.BLE_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in the Bluetooth address
+ * of the local Bluetooth adapter.
+ * <p>Always contains the extra field {@link
+ * #EXTRA_BLUETOOTH_ADDRESS} containing the Bluetooth address.
+ *
+ * Note: only system level processes are allowed to send this
+ * defined broadcast.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BLUETOOTH_ADDRESS_CHANGED =
+ "android.bluetooth.adapter.action.BLUETOOTH_ADDRESS_CHANGED";
+
+ /**
+ * Used as a String extra field in {@link
+ * #ACTION_BLUETOOTH_ADDRESS_CHANGED} intent to store the local
+ * Bluetooth address.
+ *
+ * @hide
+ */
+ public static final String EXTRA_BLUETOOTH_ADDRESS =
+ "android.bluetooth.adapter.extra.BLUETOOTH_ADDRESS";
+
+ /**
+ * Broadcast Action: The notifys Bluetooth ACL connected event. This will be
+ * by BLE Always on enabled application to know the ACL_CONNECTED event
+ * when Bluetooth state in STATE_BLE_ON. This denotes GATT connection
+ * as Bluetooth LE is the only feature available in STATE_BLE_ON
+ *
+ * This is counterpart of {@link BluetoothDevice#ACTION_ACL_CONNECTED} which
+ * works in Bluetooth state STATE_ON
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BLE_ACL_CONNECTED =
+ "android.bluetooth.adapter.action.BLE_ACL_CONNECTED";
+
+ /**
+ * Broadcast Action: The notifys Bluetooth ACL connected event. This will be
+ * by BLE Always on enabled application to know the ACL_DISCONNECTED event
+ * when Bluetooth state in STATE_BLE_ON. This denotes GATT disconnection as Bluetooth
+ * LE is the only feature available in STATE_BLE_ON
+ *
+ * This is counterpart of {@link BluetoothDevice#ACTION_ACL_DISCONNECTED} which
+ * works in Bluetooth state STATE_ON
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BLE_ACL_DISCONNECTED =
+ "android.bluetooth.adapter.action.BLE_ACL_DISCONNECTED";
+
+ /** The profile is in disconnected state */
+ public static final int STATE_DISCONNECTED =
+ 0; //BluetoothProtoEnums.CONNECTION_STATE_DISCONNECTED;
+ /** The profile is in connecting state */
+ public static final int STATE_CONNECTING = 1; //BluetoothProtoEnums.CONNECTION_STATE_CONNECTING;
+ /** The profile is in connected state */
+ public static final int STATE_CONNECTED = 2; //BluetoothProtoEnums.CONNECTION_STATE_CONNECTED;
+ /** The profile is in disconnecting state */
+ public static final int STATE_DISCONNECTING =
+ 3; //BluetoothProtoEnums.CONNECTION_STATE_DISCONNECTING;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_DISCONNECTED,
+ STATE_CONNECTING,
+ STATE_CONNECTED,
+ STATE_DISCONNECTING,
+ })
+ public @interface ConnectionState {}
+
+ /**
+ * Audio mode representing output only.
+ * @hide
+ */
+ @SystemApi
+ public static final String AUDIO_MODE_OUTPUT_ONLY = "audio_mode_output_only";
+
+ /**
+ * Audio mode representing both output and microphone input.
+ * @hide
+ */
+ @SystemApi
+ public static final String AUDIO_MODE_DUPLEX = "audio_mode_duplex";
+
+ /** @hide */
+ public static final String BLUETOOTH_MANAGER_SERVICE = "bluetooth_manager";
+ private final IBinder mToken;
+
+
+ /**
+ * When creating a ServerSocket using listenUsingRfcommOn() or
+ * listenUsingL2capOn() use SOCKET_CHANNEL_AUTO_STATIC to create
+ * a ServerSocket that auto assigns a channel number to the first
+ * bluetooth socket.
+ * The channel number assigned to this first Bluetooth Socket will
+ * be stored in the ServerSocket, and reused for subsequent Bluetooth
+ * sockets.
+ *
+ * @hide
+ */
+ public static final int SOCKET_CHANNEL_AUTO_STATIC_NO_SDP = -2;
+
+
+ private static final int ADDRESS_LENGTH = 17;
+
+ /**
+ * Lazily initialized singleton. Guaranteed final after first object
+ * constructed.
+ */
+ private static BluetoothAdapter sAdapter;
+
+ private BluetoothLeScanner mBluetoothLeScanner;
+ private BluetoothLeAdvertiser mBluetoothLeAdvertiser;
+ private PeriodicAdvertisingManager mPeriodicAdvertisingManager;
+ private DistanceMeasurementManager mDistanceMeasurementManager;
+
+ private final IBluetoothManager mManagerService;
+ private final AttributionSource mAttributionSource;
+
+ // Yeah, keeping both mService and sService isn't pretty, but it's too late
+ // in the current release for a major refactoring, so we leave them both
+ // intact until this can be cleaned up in a future release
+
+ @UnsupportedAppUsage
+ @GuardedBy("mServiceLock")
+ private IBluetooth mService;
+ private final ReentrantReadWriteLock mServiceLock = new ReentrantReadWriteLock();
+
+ @GuardedBy("sServiceLock")
+ private static boolean sServiceRegistered;
+ @GuardedBy("sServiceLock")
+ private static IBluetooth sService;
+ private static final Object sServiceLock = new Object();
+
+ private final Object mLock = new Object();
+ private final Map<LeScanCallback, ScanCallback> mLeScanClients;
+ private final Map<BluetoothDevice, List<Pair<OnMetadataChangedListener, Executor>>>
+ mMetadataListeners = new HashMap<>();
+ private final Map<BluetoothConnectionCallback, Executor>
+ mBluetoothConnectionCallbackExecutorMap = new HashMap<>();
+ private final Map<PreferredAudioProfilesChangedCallback, Executor>
+ mAudioProfilesChangedCallbackExecutorMap = new HashMap<>();
+ private final Map<BluetoothQualityReportReadyCallback, Executor>
+ mBluetoothQualityReportReadyCallbackExecutorMap = new HashMap<>();
+
+ /**
+ * Bluetooth metadata listener. Overrides the default BluetoothMetadataListener
+ * implementation.
+ */
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothMetadataListener mBluetoothMetadataListener =
+ new IBluetoothMetadataListener.Stub() {
+ @Override
+ public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ synchronized (mMetadataListeners) {
+ if (mMetadataListeners.containsKey(device)) {
+ List<Pair<OnMetadataChangedListener, Executor>> list =
+ mMetadataListeners.get(device);
+ for (Pair<OnMetadataChangedListener, Executor> pair : list) {
+ OnMetadataChangedListener listener = pair.first;
+ Executor executor = pair.second;
+ executor.execute(() -> {
+ listener.onMetadataChanged(device, key, value);
+ });
+ }
+ }
+ }
+ return;
+ }
+ };
+
+ /** @hide */
+ @IntDef(value = {
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BluetoothActivityEnergyInfoCallbackError {}
+
+ /**
+ * Interface for Bluetooth activity energy info callback. Should be implemented by applications
+ * and set when calling {@link #requestControllerActivityEnergyInfo}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface OnBluetoothActivityEnergyInfoCallback {
+ /**
+ * Called when Bluetooth activity energy info is available.
+ * Note: this callback is triggered at most once for each call to
+ * {@link #requestControllerActivityEnergyInfo}.
+ *
+ * @param info the latest {@link BluetoothActivityEnergyInfo}
+ */
+ void onBluetoothActivityEnergyInfoAvailable(
+ @NonNull BluetoothActivityEnergyInfo info);
+
+ /**
+ * Called when the latest {@link BluetoothActivityEnergyInfo} can't be retrieved.
+ * The reason of the failure is indicated by the {@link BluetoothStatusCodes}
+ * passed as an argument to this method.
+ * Note: this callback is triggered at most once for each call to
+ * {@link #requestControllerActivityEnergyInfo}.
+ *
+ * @param error code indicating the reason for the failure
+ */
+ void onBluetoothActivityEnergyInfoError(
+ @BluetoothActivityEnergyInfoCallbackError int error);
+ }
+
+ private static class OnBluetoothActivityEnergyInfoProxy
+ extends IBluetoothActivityEnergyInfoListener.Stub {
+ private final Object mLock = new Object();
+ @Nullable @GuardedBy("mLock") private Executor mExecutor;
+ @Nullable @GuardedBy("mLock") private OnBluetoothActivityEnergyInfoCallback mCallback;
+
+ OnBluetoothActivityEnergyInfoProxy(Executor executor,
+ OnBluetoothActivityEnergyInfoCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onBluetoothActivityEnergyInfoAvailable(BluetoothActivityEnergyInfo info) {
+ Executor executor;
+ OnBluetoothActivityEnergyInfoCallback callback;
+ synchronized (mLock) {
+ if (mExecutor == null || mCallback == null) {
+ return;
+ }
+ executor = mExecutor;
+ callback = mCallback;
+ mExecutor = null;
+ mCallback = null;
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (info == null) {
+ executor.execute(() -> callback.onBluetoothActivityEnergyInfoError(
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED));
+ } else {
+ executor.execute(() -> callback.onBluetoothActivityEnergyInfoAvailable(info));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Framework only method that is called when the service can't be reached.
+ */
+ public void onError(int errorCode) {
+ Executor executor;
+ OnBluetoothActivityEnergyInfoCallback callback;
+ synchronized (mLock) {
+ if (mExecutor == null || mCallback == null) {
+ return;
+ }
+ executor = mExecutor;
+ callback = mCallback;
+ mExecutor = null;
+ mCallback = null;
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> callback.onBluetoothActivityEnergyInfoError(
+ errorCode));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ /**
+ * Get a handle to the default local Bluetooth adapter.
+ * <p>
+ * Currently Android only supports one Bluetooth adapter, but the API could
+ * be extended to support more. This will always return the default adapter.
+ * </p>
+ *
+ * @return the default local adapter, or null if Bluetooth is not supported
+ * on this hardware platform
+ * @deprecated this method will continue to work, but developers are
+ * strongly encouraged to migrate to using
+ * {@link BluetoothManager#getAdapter()}, since that approach
+ * enables support for {@link Context#createAttributionContext}.
+ */
+ @Deprecated
+ @RequiresNoPermission
+ public static synchronized BluetoothAdapter getDefaultAdapter() {
+ if (sAdapter == null) {
+ sAdapter = createAdapter(AttributionSource.myAttributionSource());
+ }
+ return sAdapter;
+ }
+
+ /** {@hide} */
+ public static BluetoothAdapter createAdapter(AttributionSource attributionSource) {
+ BluetoothServiceManager manager =
+ BluetoothFrameworkInitializer.getBluetoothServiceManager();
+ if (manager == null) {
+ Log.e(TAG, "BluetoothServiceManager is null");
+ return null;
+ }
+ IBluetoothManager service = IBluetoothManager.Stub.asInterface(
+ manager.getBluetoothManagerServiceRegisterer().get());
+ if (service != null) {
+ return new BluetoothAdapter(service, attributionSource);
+ } else {
+ Log.e(TAG, "Bluetooth service is null");
+ return null;
+ }
+ }
+
+ /**
+ * Use {@link #getDefaultAdapter} to get the BluetoothAdapter instance.
+ */
+ BluetoothAdapter(IBluetoothManager managerService, AttributionSource attributionSource) {
+ mManagerService = requireNonNull(managerService);
+ mAttributionSource = requireNonNull(attributionSource);
+ mServiceLock.writeLock().lock();
+ try {
+ mService = getBluetoothService(mManagerCallback);
+ } finally {
+ mServiceLock.writeLock().unlock();
+ }
+ mLeScanClients = new HashMap<LeScanCallback, ScanCallback>();
+ mToken = new Binder(DESCRIPTOR);
+ }
+
+ /**
+ * Get a {@link BluetoothDevice} object for the given Bluetooth hardware
+ * address.
+ * <p>Valid Bluetooth hardware addresses must be upper case, in big endian byte order, and in a
+ * format such as "00:11:22:33:AA:BB". The helper {@link #checkBluetoothAddress} is
+ * available to validate a Bluetooth address.
+ * <p>A {@link BluetoothDevice} will always be returned for a valid
+ * hardware address, even if this adapter has never seen that device.
+ *
+ * @param address valid Bluetooth MAC address
+ * @throws IllegalArgumentException if address is invalid
+ */
+ @RequiresNoPermission
+ public BluetoothDevice getRemoteDevice(String address) {
+ final BluetoothDevice res = new BluetoothDevice(address);
+ res.setAttributionSource(mAttributionSource);
+ return res;
+ }
+
+ /**
+ * Get a {@link BluetoothDevice} object for the given Bluetooth hardware
+ * address and addressType.
+ * <p>Valid Bluetooth hardware addresses must be upper case, in big endian byte order, and in a
+ * format such as "00:11:22:33:AA:BB". The helper {@link #checkBluetoothAddress} is
+ * available to validate a Bluetooth address.
+ * <p>A {@link BluetoothDevice} will always be returned for a valid
+ * hardware address and type, even if this adapter has never seen that device.
+ *
+ * @param address valid Bluetooth MAC address
+ * @param addressType Bluetooth address type
+ * @throws IllegalArgumentException if address is invalid
+ */
+ @RequiresNoPermission
+ @NonNull
+ public BluetoothDevice getRemoteLeDevice(@NonNull String address,
+ @AddressType int addressType) {
+ final BluetoothDevice res = new BluetoothDevice(address, addressType);
+ res.setAttributionSource(mAttributionSource);
+ return res;
+ }
+
+ /**
+ * Get a {@link BluetoothDevice} object for the given Bluetooth hardware
+ * address.
+ * <p>Valid Bluetooth hardware addresses must be 6 bytes. This method
+ * expects the address in network byte order (MSB first).
+ * <p>A {@link BluetoothDevice} will always be returned for a valid
+ * hardware address, even if this adapter has never seen that device.
+ *
+ * @param address Bluetooth MAC address (6 bytes)
+ * @throws IllegalArgumentException if address is invalid
+ */
+ @RequiresNoPermission
+ public BluetoothDevice getRemoteDevice(byte[] address) {
+ if (address == null || address.length != 6) {
+ throw new IllegalArgumentException("Bluetooth address must have 6 bytes");
+ }
+ final BluetoothDevice res = new BluetoothDevice(
+ String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X", address[0], address[1],
+ address[2], address[3], address[4], address[5]));
+ res.setAttributionSource(mAttributionSource);
+ return res;
+ }
+
+ /**
+ * Returns a {@link BluetoothLeAdvertiser} object for Bluetooth LE Advertising operations.
+ * Will return null if Bluetooth is turned off or if Bluetooth LE Advertising is not
+ * supported on this device.
+ * <p>
+ * Use {@link #isMultipleAdvertisementSupported()} to check whether LE Advertising is supported
+ * on this device before calling this method.
+ */
+ @RequiresNoPermission
+ public BluetoothLeAdvertiser getBluetoothLeAdvertiser() {
+ if (!getLeAccess()) {
+ return null;
+ }
+ synchronized (mLock) {
+ if (mBluetoothLeAdvertiser == null) {
+ mBluetoothLeAdvertiser = new BluetoothLeAdvertiser(this);
+ }
+ return mBluetoothLeAdvertiser;
+ }
+ }
+
+ /**
+ * Returns a {@link PeriodicAdvertisingManager} object for Bluetooth LE Periodic Advertising
+ * operations. Will return null if Bluetooth is turned off or if Bluetooth LE Periodic
+ * Advertising is not supported on this device.
+ * <p>
+ * Use {@link #isLePeriodicAdvertisingSupported()} to check whether LE Periodic Advertising is
+ * supported on this device before calling this method.
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public PeriodicAdvertisingManager getPeriodicAdvertisingManager() {
+ if (!getLeAccess()) {
+ return null;
+ }
+
+ if (!isLePeriodicAdvertisingSupported()) {
+ return null;
+ }
+
+ synchronized (mLock) {
+ if (mPeriodicAdvertisingManager == null) {
+ mPeriodicAdvertisingManager = new PeriodicAdvertisingManager(this);
+ }
+ return mPeriodicAdvertisingManager;
+ }
+ }
+
+ /**
+ * Returns a {@link BluetoothLeScanner} object for Bluetooth LE scan operations.
+ */
+ @RequiresNoPermission
+ public BluetoothLeScanner getBluetoothLeScanner() {
+ if (!getLeAccess()) {
+ return null;
+ }
+ synchronized (mLock) {
+ if (mBluetoothLeScanner == null) {
+ mBluetoothLeScanner = new BluetoothLeScanner(this);
+ }
+ return mBluetoothLeScanner;
+ }
+ }
+
+ /**
+ * Get a {@link DistanceMeasurementManager} object for distance measurement operations.
+ * <p>
+ * Use {@link #isDistanceMeasurementSupported()} to check whether distance
+ * measurement is supported on this device before calling this method.
+ *
+ * @return a new instance of {@link DistanceMeasurementManager}, or {@code null} if Bluetooth is
+ * turned off
+ * @throws UnsupportedOperationException if distance measurement is not supported on this device
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Nullable DistanceMeasurementManager getDistanceMeasurementManager() {
+ if (!getLeAccess()) {
+ return null;
+ }
+
+ if (isDistanceMeasurementSupported() != BluetoothStatusCodes.FEATURE_SUPPORTED) {
+ throw new UnsupportedOperationException("Distance measurement is unsupported");
+ }
+
+ synchronized (mLock) {
+ if (mDistanceMeasurementManager == null) {
+ mDistanceMeasurementManager = new DistanceMeasurementManager(this);
+ }
+ return mDistanceMeasurementManager;
+ }
+ }
+
+ /**
+ * Return true if Bluetooth is currently enabled and ready for use.
+ * <p>Equivalent to:
+ * <code>getBluetoothState() == STATE_ON</code>
+ *
+ * @return true if the local adapter is turned on
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isEnabled() {
+ return getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ /**
+ * Return true if Bluetooth LE(Always BLE On feature) is currently
+ * enabled and ready for use
+ * <p>This returns true if current state is either STATE_ON or STATE_BLE_ON
+ *
+ * @return true if the local Bluetooth LE adapter is turned on
+ * @hide
+ */
+ @SystemApi
+ @RequiresNoPermission
+ public boolean isLeEnabled() {
+ final int state = getLeState();
+ if (DBG) {
+ Log.d(TAG, "isLeEnabled(): " + BluetoothAdapter.nameForState(state));
+ }
+ return (state == BluetoothAdapter.STATE_ON
+ || state == BluetoothAdapter.STATE_BLE_ON
+ || state == BluetoothAdapter.STATE_TURNING_ON
+ || state == BluetoothAdapter.STATE_TURNING_OFF);
+ }
+
+ /**
+ * Turns off Bluetooth LE which was earlier turned on by calling enableBLE().
+ *
+ * <p> If the internal Adapter state is STATE_BLE_ON, this would trigger the transition
+ * to STATE_OFF and completely shut-down Bluetooth
+ *
+ * <p> If the Adapter state is STATE_ON, This would unregister the existance of
+ * special Bluetooth LE application and hence the further turning off of Bluetooth
+ * from UI would ensure the complete turn-off of Bluetooth rather than staying back
+ * BLE only state
+ *
+ * <p>This is an asynchronous call: it will return immediately, and
+ * clients should listen for {@link #ACTION_BLE_STATE_CHANGED}
+ * to be notified of subsequent adapter state changes If this call returns
+ * true, then the adapter state will immediately transition from {@link
+ * #STATE_ON} to {@link #STATE_TURNING_OFF}, and some time
+ * later transition to either {@link #STATE_BLE_ON} or {@link
+ * #STATE_OFF} based on the existance of the further Always BLE ON enabled applications
+ * If this call returns false then there was an
+ * immediate problem that will prevent the QAdapter from being turned off -
+ * such as the QAadapter already being turned off.
+ *
+ * @return true to indicate success, or false on immediate error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disableBLE() {
+ if (!isBleScanAlwaysAvailable()) {
+ return false;
+ }
+ try {
+ return mManagerService.disableBle(mAttributionSource, mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Applications who want to only use Bluetooth Low Energy (BLE) can call enableBLE.
+ *
+ * enableBLE registers the existence of an app using only LE functions.
+ *
+ * enableBLE may enable Bluetooth to an LE only mode so that an app can use
+ * LE related features (BluetoothGatt or BluetoothGattServer classes)
+ *
+ * If the user disables Bluetooth while an app is registered to use LE only features,
+ * Bluetooth will remain on in LE only mode for the app.
+ *
+ * When Bluetooth is in LE only mode, it is not shown as ON to the UI.
+ *
+ * <p>This is an asynchronous call: it returns immediately, and
+ * clients should listen for {@link #ACTION_BLE_STATE_CHANGED}
+ * to be notified of adapter state changes.
+ *
+ * If this call returns * true, then the adapter state is either in a mode where
+ * LE is available, or will transition from {@link #STATE_OFF} to {@link #STATE_BLE_TURNING_ON},
+ * and some time later transition to either {@link #STATE_OFF} or {@link #STATE_BLE_ON}.
+ *
+ * If this call returns false then there was an immediate problem that prevents the
+ * adapter from being turned on - such as Airplane mode.
+ *
+ * {@link #ACTION_BLE_STATE_CHANGED} returns the Bluetooth Adapter's various
+ * states, It includes all the classic Bluetooth Adapter states along with
+ * internal BLE only states
+ *
+ * @return true to indicate Bluetooth LE will be available, or false on immediate error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean enableBLE() {
+ if (!isBleScanAlwaysAvailable()) {
+ return false;
+ }
+ try {
+ return mManagerService.enableBle(mAttributionSource, mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+
+ return false;
+ }
+
+ /**
+ * There are several instances of IpcDataCache used in this class.
+ * BluetoothCache wraps up the common code. All caches are created with a maximum of
+ * eight entries, and the key is in the bluetooth module. The name is set to the api.
+ */
+ private static class BluetoothCache<Q, R> extends IpcDataCache<Q, R> {
+ BluetoothCache(String api, IpcDataCache.QueryHandler query) {
+ super(8, IpcDataCache.MODULE_BLUETOOTH, api, api, query);
+ }};
+
+ /**
+ * Invalidate a bluetooth cache. This method is just a short-hand wrapper that
+ * enforces the bluetooth module.
+ */
+ private static void invalidateCache(@NonNull String api) {
+ IpcDataCache.invalidateCache(IpcDataCache.MODULE_BLUETOOTH, api);
+ }
+
+ private static final IpcDataCache.QueryHandler<IBluetooth, Integer> sBluetoothGetStateQuery =
+ new IpcDataCache.QueryHandler<>() {
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ @Override
+ public @InternalAdapterState Integer apply(IBluetooth serviceQuery) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ serviceQuery.getState(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothAdapter.STATE_OFF);
+ } catch (RemoteException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ private static final String GET_STATE_API = "BluetoothAdapter_getState";
+
+ private static final IpcDataCache<IBluetooth, Integer> sBluetoothGetStateCache =
+ new BluetoothCache<>(GET_STATE_API, sBluetoothGetStateQuery);
+
+ /** @hide */
+ @RequiresNoPermission
+ public void disableBluetoothGetStateCache() {
+ sBluetoothGetStateCache.disableForCurrentProcess();
+ }
+
+ /** @hide */
+ public static void invalidateBluetoothGetStateCache() {
+ invalidateCache(GET_STATE_API);
+ }
+
+ /**
+ * Fetch the current bluetooth state. If the service is down, return
+ * OFF.
+ */
+ private @InternalAdapterState int getStateInternal() {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ return sBluetoothGetStateCache.query(mService);
+ }
+ } catch (RuntimeException e) {
+ if (!(e.getCause() instanceof TimeoutException)
+ && !(e.getCause() instanceof RemoteException)) {
+ throw e;
+ }
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return STATE_OFF;
+ }
+
+ /**
+ * Get the current state of the local Bluetooth adapter.
+ * <p>Possible return values are
+ * {@link #STATE_OFF},
+ * {@link #STATE_TURNING_ON},
+ * {@link #STATE_ON},
+ * {@link #STATE_TURNING_OFF}.
+ *
+ * @return current state of Bluetooth adapter
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public @AdapterState int getState() {
+ int state = getStateInternal();
+
+ // Consider all internal states as OFF
+ if (state == BluetoothAdapter.STATE_BLE_ON || state == BluetoothAdapter.STATE_BLE_TURNING_ON
+ || state == BluetoothAdapter.STATE_BLE_TURNING_OFF) {
+ if (VDBG) {
+ Log.d(TAG, "Consider " + BluetoothAdapter.nameForState(state) + " state as OFF");
+ }
+ state = BluetoothAdapter.STATE_OFF;
+ }
+ if (VDBG) {
+ Log.d(TAG, "" + hashCode() + ": getState(). Returning " + BluetoothAdapter.nameForState(
+ state));
+ }
+ return state;
+ }
+
+ /**
+ * Get the current state of the local Bluetooth adapter
+ * <p>This returns current internal state of Adapter including LE ON/OFF
+ *
+ * <p>Possible return values are
+ * {@link #STATE_OFF},
+ * {@link #STATE_BLE_TURNING_ON},
+ * {@link #STATE_BLE_ON},
+ * {@link #STATE_TURNING_ON},
+ * {@link #STATE_ON},
+ * {@link #STATE_TURNING_OFF},
+ * {@link #STATE_BLE_TURNING_OFF}.
+ *
+ * @return current state of Bluetooth adapter
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ @UnsupportedAppUsage(publicAlternatives = "Use {@link #getState()} instead to determine "
+ + "whether you can use BLE & BT classic.")
+ public @InternalAdapterState int getLeState() {
+ int state = getStateInternal();
+
+ if (VDBG) {
+ Log.d(TAG, "getLeState() returning " + BluetoothAdapter.nameForState(state));
+ }
+ return state;
+ }
+
+ boolean getLeAccess() {
+ if (getLeState() == STATE_ON) {
+ return true;
+ } else if (getLeState() == STATE_BLE_ON) {
+ return true; // TODO: FILTER SYSTEM APPS HERE <--
+ }
+
+ return false;
+ }
+
+ /**
+ * Turn on the local Bluetooth adapter—do not use without explicit
+ * user action to turn on Bluetooth.
+ * <p>This powers on the underlying Bluetooth hardware, and starts all
+ * Bluetooth system services.
+ * <p class="caution"><strong>Bluetooth should never be enabled without
+ * direct user consent</strong>. If you want to turn on Bluetooth in order
+ * to create a wireless connection, you should use the {@link
+ * #ACTION_REQUEST_ENABLE} Intent, which will raise a dialog that requests
+ * user permission to turn on Bluetooth. The {@link #enable()} method is
+ * provided only for applications that include a user interface for changing
+ * system settings, such as a "power manager" app.</p>
+ * <p>This is an asynchronous call: it will return immediately, and
+ * clients should listen for {@link #ACTION_STATE_CHANGED}
+ * to be notified of subsequent adapter state changes. If this call returns
+ * true, then the adapter state will immediately transition from {@link
+ * #STATE_OFF} to {@link #STATE_TURNING_ON}, and some time
+ * later transition to either {@link #STATE_OFF} or {@link
+ * #STATE_ON}. If this call returns false then there was an
+ * immediate problem that will prevent the adapter from being turned on -
+ * such as Airplane mode, or the adapter is already turned on.
+ *
+ * @return true to indicate adapter startup has begun, or false on immediate error
+ *
+ * @deprecated Starting with {@link android.os.Build.VERSION_CODES#TIRAMISU}, applications
+ * are not allowed to enable/disable Bluetooth.
+ * <b>Compatibility Note:</b> For applications targeting
+ * {@link android.os.Build.VERSION_CODES#TIRAMISU} or above, this API will always fail and return
+ * {@code false}. If apps are targeting an older SDK ({@link android.os.Build.VERSION_CODES#S}
+ * or below), they can continue to use this API.
+ * <p>
+ * Deprecation Exemptions:
+ * <ul>
+ * <li>Device Owner (DO), Profile Owner (PO) and system apps.
+ * </ul>
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean enable() {
+ if (isEnabled()) {
+ if (DBG) {
+ Log.d(TAG, "enable(): BT already enabled!");
+ }
+ return true;
+ }
+ try {
+ return mManagerService.enable(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Turn off the local Bluetooth adapter—do not use without explicit
+ * user action to turn off Bluetooth.
+ * <p>This gracefully shuts down all Bluetooth connections, stops Bluetooth
+ * system services, and powers down the underlying Bluetooth hardware.
+ * <p class="caution"><strong>Bluetooth should never be disabled without
+ * direct user consent</strong>. The {@link #disable()} method is
+ * provided only for applications that include a user interface for changing
+ * system settings, such as a "power manager" app.</p>
+ * <p>This is an asynchronous call: it will return immediately, and
+ * clients should listen for {@link #ACTION_STATE_CHANGED}
+ * to be notified of subsequent adapter state changes. If this call returns
+ * true, then the adapter state will immediately transition from {@link
+ * #STATE_ON} to {@link #STATE_TURNING_OFF}, and some time
+ * later transition to either {@link #STATE_OFF} or {@link
+ * #STATE_ON}. If this call returns false then there was an
+ * immediate problem that will prevent the adapter from being turned off -
+ * such as the adapter already being turned off.
+ *
+ * @return true to indicate adapter shutdown has begun, or false on immediate error
+ *
+ * @deprecated Starting with {@link android.os.Build.VERSION_CODES#TIRAMISU}, applications
+ * are not allowed to enable/disable Bluetooth.
+ * <b>Compatibility Note:</b> For applications targeting
+ * {@link android.os.Build.VERSION_CODES#TIRAMISU} or above, this API will always fail and return
+ * {@code false}. If apps are targeting an older SDK ({@link android.os.Build.VERSION_CODES#S}
+ * or below), they can continue to use this API.
+ * <p>
+ * Deprecation Exemptions:
+ * <ul>
+ * <li>Device Owner (DO), Profile Owner (PO) and system apps.
+ * </ul>
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disable() {
+ return disable(true);
+ }
+
+ /**
+ * Turn off the local Bluetooth adapter and don't persist the setting.
+ *
+ * @param persist Indicate whether the off state should be persisted following the next reboot
+ * @return true to indicate adapter shutdown has begun, or false on immediate error
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean disable(boolean persist) {
+ try {
+ return mManagerService.disable(mAttributionSource, persist);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the hardware address of the local Bluetooth adapter.
+ * <p>For example, "00:11:22:AA:BB:CC".
+ *
+ * @return Bluetooth hardware address as string
+ *
+ * Requires {@code android.Manifest.permission#LOCAL_MAC_ADDRESS} and
+ * {@link android.Manifest.permission#BLUETOOTH_CONNECT}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ public String getAddress() {
+ try {
+ return mManagerService.getAddress(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /**
+ * Get the friendly Bluetooth name of the local Bluetooth adapter.
+ * <p>This name is visible to remote Bluetooth devices.
+ *
+ * @return the Bluetooth name, or null on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public String getName() {
+ try {
+ return mManagerService.getName(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /** {@hide} */
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public int getNameLengthForAdvertise() {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.getNameLengthForAdvertise(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(-1);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return -1;
+ }
+
+ /**
+ * Factory reset bluetooth settings.
+ *
+ * @return true to indicate that the config file was successfully cleared
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean clearBluetooth() {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.factoryReset(mAttributionSource, recv);
+ if (recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false)
+ && mManagerService != null
+ && mManagerService.onFactoryReset(mAttributionSource)) {
+ return true;
+ }
+ }
+ Log.e(TAG, "factoryReset(): Setting persist.bluetooth.factoryreset to retry later");
+ BluetoothProperties.factory_reset(true);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * See {@link #clearBluetooth()}
+ *
+ * @return true to indicate that the config file was successfully cleared
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean factoryReset() {
+ return clearBluetooth();
+ }
+
+ /**
+ * Get the UUIDs supported by the local Bluetooth adapter.
+ *
+ * @return the UUIDs supported by the local Bluetooth Adapter.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull ParcelUuid[] getUuids() {
+ List<ParcelUuid> parcels = getUuidsList();
+ return parcels.toArray(new ParcelUuid[parcels.size()]);
+ }
+
+ /**
+ * Get the UUIDs supported by the local Bluetooth adapter.
+ *
+ * @return a list of the UUIDs supported by the local Bluetooth Adapter.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull List<ParcelUuid> getUuidsList() {
+ List<ParcelUuid> defaultValue = new ArrayList<>();
+ if (getState() != STATE_ON) {
+ return defaultValue;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<List<ParcelUuid>> recv =
+ SynchronousResultReceiver.get();
+ mService.getUuids(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set the friendly Bluetooth name of the local Bluetooth adapter.
+ * <p>This name is visible to remote Bluetooth devices.
+ * <p>Valid Bluetooth names are a maximum of 248 bytes using UTF-8
+ * encoding, although many remote devices can only display the first
+ * 40 characters, and some may be limited to just 20.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @param name a valid Bluetooth name
+ * @return true if the name was set, false otherwise
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setName(String name) {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.setName(name, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the Input/Output capability of the device for classic Bluetooth.
+ *
+ * @return Input/Output capability of the device. One of {@link #IO_CAPABILITY_OUT},
+ * {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_IN}, {@link #IO_CAPABILITY_NONE},
+ * {@link #IO_CAPABILITY_KBDISP} or {@link #IO_CAPABILITY_UNKNOWN}.
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @IoCapability
+ public int getIoCapability() {
+ if (getState() != STATE_ON) return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ mService.getIoCapability(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothAdapter.IO_CAPABILITY_UNKNOWN);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ }
+
+ /**
+ * Sets the Input/Output capability of the device for classic Bluetooth.
+ *
+ * <p>Changing the Input/Output capability of a device only takes effect on restarting the
+ * Bluetooth stack. You would need to restart the stack using {@link BluetoothAdapter#disable()}
+ * and {@link BluetoothAdapter#enable()} to see the changes.
+ *
+ * @param capability Input/Output capability of the device. One of {@link #IO_CAPABILITY_OUT},
+ * {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_IN},
+ * {@link #IO_CAPABILITY_NONE} or {@link #IO_CAPABILITY_KBDISP}.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setIoCapability(@IoCapability int capability) {
+ if (getState() != STATE_ON) return false;
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.setIoCapability(capability, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Get the current Bluetooth scan mode of the local Bluetooth adapter.
+ * <p>The Bluetooth scan mode determines if the local adapter is
+ * connectable and/or discoverable from remote Bluetooth devices.
+ * <p>Possible values are:
+ * {@link #SCAN_MODE_NONE},
+ * {@link #SCAN_MODE_CONNECTABLE},
+ * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return {@link #SCAN_MODE_NONE}. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @return scan mode
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @ScanMode
+ public int getScanMode() {
+ if (getState() != STATE_ON) {
+ return SCAN_MODE_NONE;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.getScanMode(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(SCAN_MODE_NONE);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return SCAN_MODE_NONE;
+ }
+
+ /**
+ * Set the local Bluetooth adapter connectablility and discoverability.
+ * <p>If the scan mode is set to {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE},
+ * it will change to {@link #SCAN_MODE_CONNECTABLE} after the discoverable timeout.
+ * The discoverable timeout can be set with {@link #setDiscoverableTimeout} and
+ * checked with {@link #getDiscoverableTimeout}. By default, the timeout is usually
+ * 120 seconds on phones which is enough for a remote device to initiate and complete
+ * its discovery process.
+ * <p>Applications cannot set the scan mode. They should use
+ * {@link #ACTION_REQUEST_DISCOVERABLE} instead.
+ *
+ * @param mode represents the desired state of the local device scan mode
+ *
+ * @return status code indicating whether the scan mode was successfully set
+ * @throws IllegalArgumentException if the mode is not a valid scan mode
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @ScanModeStatusCode
+ public int setScanMode(@ScanMode int mode) {
+ if (getState() != STATE_ON) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ if (mode != SCAN_MODE_NONE && mode != SCAN_MODE_CONNECTABLE
+ && mode != SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ throw new IllegalArgumentException("Invalid scan mode param value");
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.setScanMode(mode, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_UNKNOWN);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Get the timeout duration of the {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}.
+ *
+ * @return the duration of the discoverable timeout or null if an error has occurred
+ */
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public @Nullable Duration getDiscoverableTimeout() {
+ if (getState() != STATE_ON) {
+ return null;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Long> recv = SynchronousResultReceiver.get();
+ mService.getDiscoverableTimeout(mAttributionSource, recv);
+ long timeout = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue((long) -1);
+ return (timeout == -1) ? null : Duration.ofSeconds(timeout);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return null;
+ }
+
+ /**
+ * Set the total time the Bluetooth local adapter will stay discoverable when
+ * {@link #setScanMode} is called with {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE} mode.
+ * After this timeout, the scan mode will fallback to {@link #SCAN_MODE_CONNECTABLE}.
+ * <p>If <code>timeout</code> is set to 0, no timeout will occur and the scan mode will
+ * be persisted until a subsequent call to {@link #setScanMode}.
+ *
+ * @param timeout represents the total duration the local Bluetooth adapter will remain
+ * discoverable, or no timeout if set to 0
+ * @return whether the timeout was successfully set
+ * @throws IllegalArgumentException if <code>timeout</code> duration in seconds is more
+ * than {@link Integer#MAX_VALUE}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @ScanModeStatusCode
+ public int setDiscoverableTimeout(@NonNull Duration timeout) {
+ if (getState() != STATE_ON) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ if (timeout.toSeconds() > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("Timeout in seconds must be less or equal to "
+ + Integer.MAX_VALUE);
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.setDiscoverableTimeout(timeout.toSeconds(), mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_UNKNOWN);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Get the end time of the latest remote device discovery process.
+ *
+ * @return the latest time that the bluetooth adapter was/will be in discovery mode, in
+ * milliseconds since the epoch. This time can be in the future if {@link #startDiscovery()} has
+ * been called recently.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public long getDiscoveryEndMillis() {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Long> recv = SynchronousResultReceiver.get();
+ mService.getDiscoveryEndMillis(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue((long) -1);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return -1;
+ }
+
+ /**
+ * Start the remote device discovery process.
+ * <p>The discovery process usually involves an inquiry scan of about 12
+ * seconds, followed by a page scan of each new device to retrieve its
+ * Bluetooth name.
+ * <p>This is an asynchronous call, it will return immediately. Register
+ * for {@link #ACTION_DISCOVERY_STARTED} and {@link
+ * #ACTION_DISCOVERY_FINISHED} intents to determine exactly when the
+ * discovery starts and completes. Register for {@link
+ * BluetoothDevice#ACTION_FOUND} to be notified as remote Bluetooth devices
+ * are found.
+ * <p>Device discovery is a heavyweight procedure. New connections to
+ * remote Bluetooth devices should not be attempted while discovery is in
+ * progress, and existing connections will experience limited bandwidth
+ * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing
+ * discovery. Discovery is not managed by the Activity,
+ * but is run as a system service, so an application should always call
+ * {@link BluetoothAdapter#cancelDiscovery()} even if it
+ * did not directly request a discovery, just to be sure.
+ * <p>Device discovery will only find remote devices that are currently
+ * <i>discoverable</i> (inquiry scan enabled). Many Bluetooth devices are
+ * not discoverable by default, and need to be entered into a special mode.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth, wait for {@link #ACTION_STATE_CHANGED}
+ * with {@link #STATE_ON} to get the updated value.
+ * <p>If a device is currently bonding, this request will be queued and executed once that
+ * device has finished bonding. If a request is already queued, this request will be ignored.
+ *
+ * @return true on success, false on error
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean startDiscovery() {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.startDiscovery(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Cancel the current device discovery process.
+ * <p>Because discovery is a heavyweight procedure for the Bluetooth
+ * adapter, this method should always be called before attempting to connect
+ * to a remote device with {@link
+ * android.bluetooth.BluetoothSocket#connect()}. Discovery is not managed by
+ * the Activity, but is run as a system service, so an application should
+ * always call cancel discovery even if it did not directly request a
+ * discovery, just to be sure.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @return true on success, false on error
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean cancelDiscovery() {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.cancelDiscovery(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if the local Bluetooth adapter is currently in the device
+ * discovery process.
+ * <p>Device discovery is a heavyweight procedure. New connections to
+ * remote Bluetooth devices should not be attempted while discovery is in
+ * progress, and existing connections will experience limited bandwidth
+ * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing
+ * discovery.
+ * <p>Applications can also register for {@link #ACTION_DISCOVERY_STARTED}
+ * or {@link #ACTION_DISCOVERY_FINISHED} to be notified when discovery
+ * starts or completes.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @return true if discovering
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean isDiscovering() {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.isDiscovering(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Removes the active device for the grouping of @ActiveDeviceUse specified
+ *
+ * @param profiles represents the purpose for which we are setting this as the active device.
+ * Possible values are:
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_AUDIO},
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_PHONE_CALL},
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_ALL}
+ * @return false on immediate error, true otherwise
+ * @throws IllegalArgumentException if device is null or profiles is not one of
+ * {@link ActiveDeviceUse}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public boolean removeActiveDevice(@ActiveDeviceUse int profiles) {
+ if (profiles != ACTIVE_DEVICE_AUDIO && profiles != ACTIVE_DEVICE_PHONE_CALL
+ && profiles != ACTIVE_DEVICE_ALL) {
+ Log.e(TAG, "Invalid profiles param value in removeActiveDevice");
+ throw new IllegalArgumentException("Profiles must be one of "
+ + "BluetoothAdapter.ACTIVE_DEVICE_AUDIO, "
+ + "BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL, or "
+ + "BluetoothAdapter.ACTIVE_DEVICE_ALL");
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ if (DBG) Log.d(TAG, "removeActiveDevice, profiles: " + profiles);
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.removeActiveDevice(profiles, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets device as the active devices for the use cases passed into the function. Note that in
+ * order to make a device active for LE Audio, it must be the active device for audio and
+ * phone calls.
+ *
+ * @param device is the remote bluetooth device
+ * @param profiles represents the purpose for which we are setting this as the active device.
+ * Possible values are:
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_AUDIO},
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_PHONE_CALL},
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_ALL}
+ * @return false on immediate error, true otherwise
+ * @throws IllegalArgumentException if device is null or profiles is not one of
+ * {@link ActiveDeviceUse}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public boolean setActiveDevice(@NonNull BluetoothDevice device,
+ @ActiveDeviceUse int profiles) {
+ if (device == null) {
+ Log.e(TAG, "setActiveDevice: Null device passed as parameter");
+ throw new IllegalArgumentException("device cannot be null");
+ }
+ if (profiles != ACTIVE_DEVICE_AUDIO && profiles != ACTIVE_DEVICE_PHONE_CALL
+ && profiles != ACTIVE_DEVICE_ALL) {
+ Log.e(TAG, "Invalid profiles param value in setActiveDevice");
+ throw new IllegalArgumentException("Profiles must be one of "
+ + "BluetoothAdapter.ACTIVE_DEVICE_AUDIO, "
+ + "BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL, or "
+ + "BluetoothAdapter.ACTIVE_DEVICE_ALL");
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ if (DBG) {
+ Log.d(TAG, "setActiveDevice, device: " + device + ", profiles: " + profiles);
+ }
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.setActiveDevice(device, profiles, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the active devices for the BluetoothProfile specified
+ *
+ * @param profile is the profile from which we want the active devices.
+ * Possible values are:
+ * {@link BluetoothProfile#HEADSET},
+ * {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#HEARING_AID}
+ * {@link BluetoothProfile#LE_AUDIO}
+ * @return A list of active bluetooth devices
+ * @throws IllegalArgumentException If profile is not one of {@link ActiveDeviceProfile}
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull List<BluetoothDevice> getActiveDevices(@ActiveDeviceProfile int profile) {
+ if (profile != BluetoothProfile.HEADSET
+ && profile != BluetoothProfile.A2DP
+ && profile != BluetoothProfile.HEARING_AID
+ && profile != BluetoothProfile.LE_AUDIO) {
+ Log.e(TAG, "Invalid profile param value in getActiveDevices");
+ throw new IllegalArgumentException("Profiles must be one of "
+ + "BluetoothProfile.A2DP, "
+ + "BluetoothProfile.HEARING_AID, or"
+ + "BluetoothProfile.HEARING_AID"
+ + "BluetoothProfile.LE_AUDIO");
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ if (DBG) {
+ Log.d(TAG, "getActiveDevices(profile= "
+ + BluetoothProfile.getProfileName(profile) + ")");
+ }
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ mService.getActiveDevices(profile, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(new ArrayList<>());
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return new ArrayList<>();
+ }
+
+ /**
+ * Return true if the multi advertisement is supported by the chipset
+ *
+ * @return true if Multiple Advertisement feature is supported
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isMultipleAdvertisementSupported() {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.isMultiAdvertisementSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if BLE scan is always available, {@code false} otherwise. <p>
+ *
+ * If this returns {@code true}, application can issue {@link BluetoothLeScanner#startScan} and
+ * fetch scan results even when Bluetooth is turned off.<p>
+ *
+ * To change this setting, use {@link #ACTION_REQUEST_BLE_SCAN_ALWAYS_AVAILABLE}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresNoPermission
+ public boolean isBleScanAlwaysAvailable() {
+ try {
+ return mManagerService.isBleScanAlwaysAvailable();
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception when calling isBleScanAlwaysAvailable", e);
+ return false;
+ }
+ }
+
+ private static final IpcDataCache.QueryHandler<IBluetooth, Boolean> sBluetoothFilteringQuery =
+ new IpcDataCache.QueryHandler<>() {
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ @Override
+ public Boolean apply(IBluetooth serviceQuery) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ serviceQuery.isOffloadedFilteringSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ } catch (RemoteException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }};
+
+ private static final String FILTERING_API = "BluetoothAdapter_isOffloadedFilteringSupported";
+
+ private static final IpcDataCache<IBluetooth, Boolean> sBluetoothFilteringCache =
+ new BluetoothCache<>(FILTERING_API, sBluetoothFilteringQuery);
+
+ /** @hide */
+ @RequiresNoPermission
+ public void disableIsOffloadedFilteringSupportedCache() {
+ sBluetoothFilteringCache.disableForCurrentProcess();
+ }
+
+ /** @hide */
+ public static void invalidateIsOffloadedFilteringSupportedCache() {
+ invalidateCache(FILTERING_API);
+ }
+
+ /**
+ * Return true if offloaded filters are supported
+ *
+ * @return true if chipset supports on-chip filtering
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isOffloadedFilteringSupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) return sBluetoothFilteringCache.query(mService);
+ } catch (RuntimeException e) {
+ if (!(e.getCause() instanceof TimeoutException)
+ && !(e.getCause() instanceof RemoteException)) {
+ throw e;
+ }
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if offloaded scan batching is supported
+ *
+ * @return true if chipset supports on-chip scan batching
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isOffloadedScanBatchingSupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.isOffloadedScanBatchingSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if LE 2M PHY feature is supported.
+ *
+ * @return true if chipset supports LE 2M PHY feature
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isLe2MPhySupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.isLe2MPhySupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if LE Coded PHY feature is supported.
+ *
+ * @return true if chipset supports LE Coded PHY feature
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isLeCodedPhySupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.isLeCodedPhySupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if LE Extended Advertising feature is supported.
+ *
+ * @return true if chipset supports LE Extended Advertising feature
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isLeExtendedAdvertisingSupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.isLeExtendedAdvertisingSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if LE Periodic Advertising feature is supported.
+ *
+ * @return true if chipset supports LE Periodic Advertising feature
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isLePeriodicAdvertisingSupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.isLePeriodicAdvertisingSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.FEATURE_SUPPORTED,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED,
+ })
+ public @interface LeFeatureReturnValues {}
+
+ /**
+ * Returns {@link BluetoothStatusCodes#FEATURE_SUPPORTED} if the LE audio feature is
+ * supported, {@link BluetoothStatusCodes#FEATURE_NOT_SUPPORTED} if the feature is not
+ * supported, or an error code.
+ *
+ * @return whether the LE audio is supported
+ * @throws IllegalStateException if the bluetooth service is null
+ */
+ @RequiresNoPermission
+ public @LeFeatureReturnValues int isLeAudioSupported() {
+ if (!getLeAccess()) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.isLeAudioSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_UNKNOWN);
+ } else {
+ throw new IllegalStateException(
+ "LE state is on, but there is no bluetooth service.");
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Returns {@link BluetoothStatusCodes#FEATURE_SUPPORTED} if the LE audio broadcast source
+ * feature is supported, {@link BluetoothStatusCodes#FEATURE_NOT_SUPPORTED} if the feature
+ * is not supported, or an error code.
+ *
+ * @return whether the LE audio broadcast source is supported
+ * @throws IllegalStateException if the bluetooth service is null
+ */
+ @RequiresNoPermission
+ public @LeFeatureReturnValues int isLeAudioBroadcastSourceSupported() {
+ if (!getLeAccess()) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.isLeAudioBroadcastSourceSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_UNKNOWN);
+ } else {
+ throw new IllegalStateException(
+ "LE state is on, but there is no bluetooth service.");
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Returns {@link BluetoothStatusCodes#FEATURE_SUPPORTED} if the LE audio broadcast assistant
+ * feature is supported, {@link BluetoothStatusCodes#FEATURE_NOT_SUPPORTED} if the feature is
+ * not supported, or an error code.
+ *
+ * @return whether the LE audio broadcast assistent is supported
+ * @throws IllegalStateException if the bluetooth service is null
+ */
+ @RequiresNoPermission
+ public @LeFeatureReturnValues int isLeAudioBroadcastAssistantSupported() {
+ if (!getLeAccess()) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.isLeAudioBroadcastAssistantSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_UNKNOWN);
+ } else {
+ throw new IllegalStateException(
+ "LE state is on, but there is no bluetooth service.");
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Returns whether the distance measurement feature is supported.
+ *
+ * @return whether the Bluetooth distance measurement is supported
+ * @throws IllegalStateException if the bluetooth service is null
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @LeFeatureReturnValues int isDistanceMeasurementSupported() {
+ if (!getLeAccess()) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.isDistanceMeasurementSupported(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_UNKNOWN);
+ } else {
+ throw new IllegalStateException(
+ "LE state is on, but there is no bluetooth service.");
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Return the maximum LE advertising data length in bytes,
+ * if LE Extended Advertising feature is supported, 0 otherwise.
+ *
+ * @return the maximum LE advertising data length.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public int getLeMaximumAdvertisingDataLength() {
+ if (!getLeAccess()) {
+ return 0;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.getLeMaximumAdvertisingDataLength(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(0);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return 0;
+ }
+
+ /**
+ * Return true if Hearing Aid Profile is supported.
+ *
+ * @return true if phone supports Hearing Aid Profile
+ */
+ @RequiresNoPermission
+ private boolean isHearingAidProfileSupported() {
+ try {
+ return mManagerService.isHearingAidProfileSupported();
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception when calling isHearingAidProfileSupported", e);
+ return false;
+ }
+ }
+
+ /**
+ * Get the maximum number of connected devices per audio profile for this device.
+ *
+ * @return the number of allowed simultaneous connected devices for each audio profile
+ * for this device, or -1 if the Bluetooth service can't be reached
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getMaxConnectedAudioDevices() {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.getMaxConnectedAudioDevices(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(1);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return -1;
+ }
+
+ /**
+ * Return true if hardware has entries available for matching beacons
+ *
+ * @return true if there are hw entries available for matching beacons
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isHardwareTrackingFiltersAvailable() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ try {
+ IBluetoothGatt iGatt = mManagerService.getBluetoothGatt();
+ if (iGatt == null) {
+ // BLE is not supported
+ return false;
+ }
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ iGatt.numHwTrackFiltersAvailable(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(0) != 0;
+ } catch (TimeoutException | RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Request the record of {@link BluetoothActivityEnergyInfo} object that
+ * has the activity and energy info. This can be used to ascertain what
+ * the controller has been up to, since the last sample.
+ *
+ * The callback will be called only once, when the record is available.
+ *
+ * @param executor the executor that the callback will be invoked on
+ * @param callback the callback that will be called with either the
+ * {@link BluetoothActivityEnergyInfo} object, or the
+ * error code if an error has occurred
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void requestControllerActivityEnergyInfo(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnBluetoothActivityEnergyInfoCallback callback) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+ OnBluetoothActivityEnergyInfoProxy proxy =
+ new OnBluetoothActivityEnergyInfoProxy(executor, callback);
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ mService.requestActivityInfo(
+ proxy,
+ mAttributionSource);
+ } else {
+ proxy.onError(BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "getControllerActivityEnergyInfoCallback: " + e);
+ proxy.onError(BluetoothStatusCodes.ERROR_UNKNOWN);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Fetches a list of the most recently connected bluetooth devices ordered by how recently they
+ * were connected with most recently first and least recently last
+ *
+ * @return {@link List} of bonded {@link BluetoothDevice} ordered by how recently they were
+ * connected
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull List<BluetoothDevice> getMostRecentlyConnectedDevices() {
+ if (getState() != STATE_ON) {
+ return new ArrayList<>();
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ mService.getMostRecentlyConnectedDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(new ArrayList<>()),
+ mAttributionSource);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * Return the set of {@link BluetoothDevice} objects that are bonded
+ * (paired) to the local adapter.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return an empty set. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @return unmodifiable set of {@link BluetoothDevice}, or null on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public Set<BluetoothDevice> getBondedDevices() {
+ if (getState() != STATE_ON) {
+ return toDeviceSet(Arrays.asList());
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ mService.getBondedDevices(mAttributionSource, recv);
+ return toDeviceSet(Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(new ArrayList<>()),
+ mAttributionSource));
+ }
+ return toDeviceSet(Arrays.asList());
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the currently supported profiles by the adapter.
+ *
+ * <p> This can be used to check whether a profile is supported before attempting
+ * to connect to its respective proxy.
+ *
+ * @return a list of integers indicating the ids of supported profiles as defined in {@link
+ * BluetoothProfile}.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull List<Integer> getSupportedProfiles() {
+ final ArrayList<Integer> supportedProfiles = new ArrayList<Integer>();
+
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Long> recv = SynchronousResultReceiver.get();
+ mService.getSupportedProfiles(mAttributionSource, recv);
+ final long supportedProfilesBitMask =
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue((long) 0);
+
+ for (int i = 0; i <= BluetoothProfile.MAX_PROFILE_ID; i++) {
+ if ((supportedProfilesBitMask & (1 << i)) != 0) {
+ supportedProfiles.add(i);
+ }
+ }
+ } else {
+ // Bluetooth is disabled. Just fill in known supported Profiles
+ if (isHearingAidProfileSupported()) {
+ supportedProfiles.add(BluetoothProfile.HEARING_AID);
+ }
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return supportedProfiles;
+ }
+
+ private static final IpcDataCache.QueryHandler<IBluetooth, Integer>
+ sBluetoothGetAdapterConnectionStateQuery = new IpcDataCache.QueryHandler<>() {
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ @Override
+ public Integer apply(IBluetooth serviceQuery) {
+ try {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ serviceQuery.getAdapterConnectionState(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(STATE_DISCONNECTED);
+ } catch (RemoteException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ private static final String GET_CONNECTION_API = "BluetoothAdapter_getConnectionState";
+
+ private static final IpcDataCache<IBluetooth, Integer>
+ sBluetoothGetAdapterConnectionStateCache = new BluetoothCache<>(GET_CONNECTION_API,
+ sBluetoothGetAdapterConnectionStateQuery);
+
+ /** @hide */
+ @RequiresNoPermission
+ public void disableGetAdapterConnectionStateCache() {
+ sBluetoothGetAdapterConnectionStateCache.disableForCurrentProcess();
+ }
+
+ /** @hide */
+ public static void invalidateGetAdapterConnectionStateCache() {
+ invalidateCache(GET_CONNECTION_API);
+ }
+
+ /**
+ * Get the current connection state of the local Bluetooth adapter.
+ * This can be used to check whether the local Bluetooth adapter is connected
+ * to any profile of any other remote Bluetooth Device.
+ *
+ * <p> Use this function along with {@link #ACTION_CONNECTION_STATE_CHANGED}
+ * intent to get the connection state of the adapter.
+ *
+ * @return the connection state
+ * @hide
+ */
+ @SystemApi
+ @RequiresNoPermission
+ public @ConnectionState int getConnectionState() {
+ if (getState() != STATE_ON) {
+ return BluetoothAdapter.STATE_DISCONNECTED;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) return sBluetoothGetAdapterConnectionStateCache.query(mService);
+ } catch (RuntimeException e) {
+ if (!(e.getCause() instanceof TimeoutException)
+ && !(e.getCause() instanceof RemoteException)) {
+ throw e;
+ }
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return STATE_DISCONNECTED;
+ }
+
+ private static final IpcDataCache
+ .QueryHandler<Pair<IBluetooth, Pair<AttributionSource, Integer>>, Integer>
+ sBluetoothProfileQuery = new IpcDataCache.QueryHandler<>() {
+ @RequiresNoPermission
+ @Override
+ public Integer apply(Pair<IBluetooth, Pair<AttributionSource, Integer>> pairQuery) {
+ IBluetooth service = pairQuery.first;
+ AttributionSource source = pairQuery.second.first;
+ Integer profile = pairQuery.second.second;
+ final int defaultValue = STATE_DISCONNECTED;
+ try {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ service.getProfileConnectionState(profile, source, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ private static final String PROFILE_API = "BluetoothAdapter_getProfileConnectionState";
+
+ private static final IpcDataCache<Pair<IBluetooth, Pair<AttributionSource, Integer>>, Integer>
+ sGetProfileConnectionStateCache = new BluetoothCache<>(PROFILE_API,
+ sBluetoothProfileQuery);
+
+ /**
+ * @hide
+ */
+ @RequiresNoPermission
+ public void disableGetProfileConnectionStateCache() {
+ sGetProfileConnectionStateCache.disableForCurrentProcess();
+ }
+
+ /**
+ * @hide
+ */
+ public static void invalidateGetProfileConnectionStateCache() {
+ invalidateCache(PROFILE_API);
+ }
+
+ /**
+ * Get the current connection state of a profile.
+ * This function can be used to check whether the local Bluetooth adapter
+ * is connected to any remote device for a specific profile.
+ * Profile can be one of {@link BluetoothProfile#HEADSET}, {@link BluetoothProfile#A2DP}.
+ *
+ * <p> Return the profile connection state
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @ConnectionState int getProfileConnectionState(int profile) {
+ if (getState() != STATE_ON) {
+ return STATE_DISCONNECTED;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ return sGetProfileConnectionStateCache.query(
+ new Pair<>(mService, new Pair<>(mAttributionSource, profile)));
+ }
+ } catch (RuntimeException e) {
+ if (!(e.getCause() instanceof TimeoutException)
+ && !(e.getCause() instanceof RemoteException)) {
+ throw e;
+ }
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return STATE_DISCONNECTED;
+ }
+
+ /**
+ * Create a listening, secure RFCOMM Bluetooth socket.
+ * <p>A remote device connecting to this socket will be authenticated and
+ * communication on this socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>Valid RFCOMM channels are in range 1 to 30.
+ *
+ * @param channel RFCOMM channel to listen on
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingRfcommOn(int channel) throws IOException {
+ return listenUsingRfcommOn(channel, false, false);
+ }
+
+ /**
+ * Create a listening, secure RFCOMM Bluetooth socket.
+ * <p>A remote device connecting to this socket will be authenticated and
+ * communication on this socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>Valid RFCOMM channels are in range 1 to 30.
+ * <p>To auto assign a channel without creating a SDP record use
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as channel number.
+ *
+ * @param channel RFCOMM channel to listen on
+ * @param mitm enforce person-in-the-middle protection for authentication.
+ * @param min16DigitPin enforce a pin key length og minimum 16 digit for sec mode 2
+ * connections.
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingRfcommOn(int channel, boolean mitm,
+ boolean min16DigitPin) throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, true, true, channel, mitm,
+ min16DigitPin);
+ int errno = socket.mSocket.bindListen();
+ if (channel == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ socket.setChannel(socket.mSocket.getPort());
+ }
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+ }
+
+ /**
+ * Create a listening, secure RFCOMM Bluetooth socket with Service Record.
+ * <p>A remote device connecting to this socket will be authenticated and
+ * communication on this socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>The system will assign an unused RFCOMM channel to listen on.
+ * <p>The system will also register a Service Discovery
+ * Protocol (SDP) record with the local SDP server containing the specified
+ * UUID, service name, and auto-assigned channel. Remote Bluetooth devices
+ * can use the same UUID to query our SDP server and discover which channel
+ * to connect to. This SDP record will be removed when this socket is
+ * closed, or if this application closes unexpectedly.
+ * <p>Use {@link BluetoothDevice#createRfcommSocketToServiceRecord} to
+ * connect to this socket from another device using the same {@link UUID}.
+ *
+ * @param name service name for SDP record
+ * @param uuid uuid for SDP record
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingRfcommWithServiceRecord(String name, UUID uuid)
+ throws IOException {
+ return createNewRfcommSocketAndRecord(name, uuid, true, true);
+ }
+
+ /**
+ * Requests the framework to start an RFCOMM socket server which listens based on the provided
+ * {@code name} and {@code uuid}.
+ * <p>
+ * Incoming connections will cause the system to start the component described in the {@link
+ * PendingIntent}, {@code pendingIntent}. After the component is started, it should obtain a
+ * {@link BluetoothAdapter} and retrieve the {@link BluetoothSocket} via {@link
+ * #retrieveConnectedRfcommSocket(UUID)}.
+ * <p>
+ * An application may register multiple RFCOMM listeners. It is recommended to set the extra
+ * field {@link #EXTRA_RFCOMM_LISTENER_ID} to help determine which service record the incoming
+ * {@link BluetoothSocket} is using.
+ * <p>
+ * The provided {@link PendingIntent} must be created with the {@link
+ * PendingIntent#FLAG_IMMUTABLE} flag.
+ *
+ * @param name service name for SDP record
+ * @param uuid uuid for SDP record
+ * @param pendingIntent component which is called when a new RFCOMM connection is available
+ * @return a status code from {@link BluetoothStatusCodes}
+ * @throws IllegalArgumentException if {@code pendingIntent} is not created with the {@link
+ * PendingIntent#FLAG_IMMUTABLE} flag.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ @RfcommListenerResult
+ public int startRfcommServer(@NonNull String name, @NonNull UUID uuid,
+ @NonNull PendingIntent pendingIntent) {
+ if (!pendingIntent.isImmutable()) {
+ throw new IllegalArgumentException("The provided PendingIntent is not immutable");
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.startRfcommListener(
+ name, new ParcelUuid(uuid), pendingIntent, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "Failed to transact RFCOMM listener start request", e);
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ }
+
+ /**
+ * Closes the RFCOMM socket server listening on the given SDP record name and UUID. This can be
+ * called by applications after calling {@link #startRfcommServer(String, UUID,
+ * PendingIntent)} to stop listening for incoming RFCOMM connections.
+ *
+ * @param uuid uuid for SDP record
+ * @return a status code from {@link BluetoothStatusCodes}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @RfcommListenerResult
+ public int stopRfcommServer(@NonNull UUID uuid) {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.stopRfcommListener(new ParcelUuid(uuid), mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "Failed to transact RFCOMM listener stop request", e);
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ }
+
+ /**
+ * Retrieves a connected {@link BluetoothSocket} for the given service record from a RFCOMM
+ * listener which was registered with {@link #startRfcommServer(String, UUID, PendingIntent)}.
+ * <p>
+ * This method should be called by the component started by the {@link PendingIntent} which was
+ * registered during the call to {@link #startRfcommServer(String, UUID, PendingIntent)} in
+ * order to retrieve the socket.
+ *
+ * @param uuid the same UUID used to register the listener previously
+ * @return a connected {@link BluetoothSocket} or {@code null} if no socket is available
+ * @throws IllegalStateException if the socket could not be retrieved because the application is
+ * trying to obtain a socket for a listener it did not register (incorrect {@code
+ * uuid}).
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull BluetoothSocket retrieveConnectedRfcommSocket(@NonNull UUID uuid) {
+ IncomingRfcommSocketInfo socketInfo = null;
+
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<IncomingRfcommSocketInfo> recv =
+ SynchronousResultReceiver.get();
+ mService.retrievePendingSocketForServiceRecord(new ParcelUuid(uuid),
+ mAttributionSource, recv);
+ socketInfo = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ return null;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ if (socketInfo == null) {
+ return null;
+ }
+
+ switch (socketInfo.status) {
+ case BluetoothStatusCodes.SUCCESS:
+ try {
+ return BluetoothSocket.createSocketFromOpenFd(
+ socketInfo.pfd,
+ socketInfo.bluetoothDevice,
+ new ParcelUuid(uuid));
+ } catch (IOException e) {
+ return null;
+ }
+ case BluetoothStatusCodes.RFCOMM_LISTENER_OPERATION_FAILED_DIFFERENT_APP:
+ throw new IllegalStateException(
+ String.format(
+ "RFCOMM listener for UUID %s was not registered by this app",
+ uuid));
+ case BluetoothStatusCodes.RFCOMM_LISTENER_NO_SOCKET_AVAILABLE:
+ return null;
+ default:
+ Log.e(TAG,
+ String.format(
+ "Unexpected result: (%d), from the adapter service while retrieving"
+ + " an rfcomm socket",
+ socketInfo.status));
+ return null;
+ }
+ }
+
+ /**
+ * Create a listening, insecure RFCOMM Bluetooth socket with Service Record.
+ * <p>The link key is not required to be authenticated, i.e the communication may be
+ * vulnerable to Person In the Middle attacks. For Bluetooth 2.1 devices,
+ * the link will be encrypted, as encryption is mandatory.
+ * For legacy devices (pre Bluetooth 2.1 devices) the link will not
+ * be encrypted. Use {@link #listenUsingRfcommWithServiceRecord}, if an
+ * encrypted and authenticated communication channel is desired.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>The system will assign an unused RFCOMM channel to listen on.
+ * <p>The system will also register a Service Discovery
+ * Protocol (SDP) record with the local SDP server containing the specified
+ * UUID, service name, and auto-assigned channel. Remote Bluetooth devices
+ * can use the same UUID to query our SDP server and discover which channel
+ * to connect to. This SDP record will be removed when this socket is
+ * closed, or if this application closes unexpectedly.
+ * <p>Use {@link BluetoothDevice#createInsecureRfcommSocketToServiceRecord} to
+ * connect to this socket from another device using the same {@link UUID}.
+ *
+ * @param name service name for SDP record
+ * @param uuid uuid for SDP record
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingInsecureRfcommWithServiceRecord(String name, UUID uuid)
+ throws IOException {
+ return createNewRfcommSocketAndRecord(name, uuid, false, false);
+ }
+
+ /**
+ * Create a listening, encrypted,
+ * RFCOMM Bluetooth socket with Service Record.
+ * <p>The link will be encrypted, but the link key is not required to be authenticated
+ * i.e the communication is vulnerable to Person In the Middle attacks. Use
+ * {@link #listenUsingRfcommWithServiceRecord}, to ensure an authenticated link key.
+ * <p> Use this socket if authentication of link key is not possible.
+ * For example, for Bluetooth 2.1 devices, if any of the devices does not have
+ * an input and output capability or just has the ability to display a numeric key,
+ * a secure socket connection is not possible and this socket can be used.
+ * Use {@link #listenUsingInsecureRfcommWithServiceRecord}, if encryption is not required.
+ * For Bluetooth 2.1 devices, the link will be encrypted, as encryption is mandatory.
+ * For more details, refer to the Security Model section 5.2 (vol 3) of
+ * Bluetooth Core Specification version 2.1 + EDR.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>The system will assign an unused RFCOMM channel to listen on.
+ * <p>The system will also register a Service Discovery
+ * Protocol (SDP) record with the local SDP server containing the specified
+ * UUID, service name, and auto-assigned channel. Remote Bluetooth devices
+ * can use the same UUID to query our SDP server and discover which channel
+ * to connect to. This SDP record will be removed when this socket is
+ * closed, or if this application closes unexpectedly.
+ * <p>Use {@link BluetoothDevice#createRfcommSocketToServiceRecord} to
+ * connect to this socket from another device using the same {@link UUID}.
+ *
+ * @param name service name for SDP record
+ * @param uuid uuid for SDP record
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingEncryptedRfcommWithServiceRecord(String name, UUID uuid)
+ throws IOException {
+ return createNewRfcommSocketAndRecord(name, uuid, false, true);
+ }
+
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private BluetoothServerSocket createNewRfcommSocketAndRecord(String name, UUID uuid,
+ boolean auth, boolean encrypt) throws IOException {
+ BluetoothServerSocket socket;
+ socket = new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, auth, encrypt,
+ new ParcelUuid(uuid));
+ socket.setServiceName(name);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+ }
+
+ /**
+ * Construct an unencrypted, unauthenticated, RFCOMM server socket.
+ * Call #accept to retrieve connections to this socket.
+ *
+ * @return An RFCOMM BluetoothServerSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingInsecureRfcommOn(int port) throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, false, false, port);
+ int errno = socket.mSocket.bindListen();
+ if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ socket.setChannel(socket.mSocket.getPort());
+ }
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+ }
+
+ /**
+ * Construct an encrypted, authenticated, L2CAP server socket.
+ * Call #accept to retrieve connections to this socket.
+ * <p>To auto assign a port without creating a SDP record use
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ *
+ * @param port the PSM to listen on
+ * @param mitm enforce person-in-the-middle protection for authentication.
+ * @param min16DigitPin enforce a pin key length og minimum 16 digit for sec mode 2
+ * connections.
+ * @return An L2CAP BluetoothServerSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingL2capOn(int port, boolean mitm, boolean min16DigitPin)
+ throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, true, true, port, mitm,
+ min16DigitPin);
+ int errno = socket.mSocket.bindListen();
+ if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ int assignedChannel = socket.mSocket.getPort();
+ if (DBG) Log.d(TAG, "listenUsingL2capOn: set assigned channel to " + assignedChannel);
+ socket.setChannel(assignedChannel);
+ }
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+ }
+
+ /**
+ * Construct an encrypted, authenticated, L2CAP server socket.
+ * Call #accept to retrieve connections to this socket.
+ * <p>To auto assign a port without creating a SDP record use
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ *
+ * @param port the PSM to listen on
+ * @return An L2CAP BluetoothServerSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingL2capOn(int port) throws IOException {
+ return listenUsingL2capOn(port, false, false);
+ }
+
+ /**
+ * Construct an insecure L2CAP server socket.
+ * Call #accept to retrieve connections to this socket.
+ * <p>To auto assign a port without creating a SDP record use
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ *
+ * @param port the PSM to listen on
+ * @return An L2CAP BluetoothServerSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingInsecureL2capOn(int port) throws IOException {
+ Log.d(TAG, "listenUsingInsecureL2capOn: port=" + port);
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, false, false, port, false,
+ false);
+ int errno = socket.mSocket.bindListen();
+ if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ int assignedChannel = socket.mSocket.getPort();
+ if (DBG) {
+ Log.d(TAG, "listenUsingInsecureL2capOn: set assigned channel to "
+ + assignedChannel);
+ }
+ socket.setChannel(assignedChannel);
+ }
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+
+ }
+
+ /**
+ * Read the local Out of Band Pairing Data
+ *
+ * @return Pair<byte[], byte[]> of Hash and Randomizer
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public Pair<byte[], byte[]> readOutOfBandData() {
+ return null;
+ }
+
+ /**
+ * Get the profile proxy object associated with the profile.
+ *
+ * <p>Profile can be one of {@link BluetoothProfile#HEADSET}, {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#GATT}, {@link BluetoothProfile#HEARING_AID}, or {@link
+ * BluetoothProfile#GATT_SERVER}. Clients must implement {@link
+ * BluetoothProfile.ServiceListener} to get notified of the connection status and to get the
+ * proxy object.
+ *
+ * @param context Context of the application
+ * @param listener The service Listener for connection callbacks.
+ * @param profile The Bluetooth profile; either {@link BluetoothProfile#HEADSET},
+ * {@link BluetoothProfile#A2DP}, {@link BluetoothProfile#GATT}, {@link
+ * BluetoothProfile#HEARING_AID} or {@link BluetoothProfile#GATT_SERVER}.
+ * @return true on success, false on error
+ */
+ @SuppressLint({
+ "AndroidFrameworkRequiresPermission",
+ "AndroidFrameworkBluetoothPermission"
+ })
+ public boolean getProfileProxy(Context context, BluetoothProfile.ServiceListener listener,
+ int profile) {
+ if (context == null || listener == null) {
+ return false;
+ }
+
+ if (profile == BluetoothProfile.HEADSET) {
+ BluetoothHeadset headset = new BluetoothHeadset(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.A2DP) {
+ BluetoothA2dp a2dp = new BluetoothA2dp(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.A2DP_SINK) {
+ BluetoothA2dpSink a2dpSink = new BluetoothA2dpSink(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
+ BluetoothAvrcpController avrcp = new BluetoothAvrcpController(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HID_HOST) {
+ BluetoothHidHost iDev = new BluetoothHidHost(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.PAN) {
+ BluetoothPan pan = new BluetoothPan(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.PBAP) {
+ BluetoothPbap pbap = new BluetoothPbap(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HEALTH) {
+ Log.e(TAG, "getProfileProxy(): BluetoothHealth is deprecated");
+ return false;
+ } else if (profile == BluetoothProfile.MAP) {
+ BluetoothMap map = new BluetoothMap(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HEADSET_CLIENT) {
+ BluetoothHeadsetClient headsetClient =
+ new BluetoothHeadsetClient(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.SAP) {
+ BluetoothSap sap = new BluetoothSap(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.PBAP_CLIENT) {
+ BluetoothPbapClient pbapClient = new BluetoothPbapClient(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.MAP_CLIENT) {
+ BluetoothMapClient mapClient = new BluetoothMapClient(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HID_DEVICE) {
+ BluetoothHidDevice hidDevice = new BluetoothHidDevice(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HAP_CLIENT) {
+ BluetoothHapClient HapClient = new BluetoothHapClient(context, listener);
+ return true;
+ } else if (profile == BluetoothProfile.HEARING_AID) {
+ if (isHearingAidProfileSupported()) {
+ BluetoothHearingAid hearingAid = new BluetoothHearingAid(context, listener, this);
+ return true;
+ }
+ return false;
+ } else if (profile == BluetoothProfile.LE_AUDIO) {
+ BluetoothLeAudio leAudio = new BluetoothLeAudio(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.LE_AUDIO_BROADCAST) {
+ BluetoothLeBroadcast leAudio = new BluetoothLeBroadcast(context, listener);
+ return true;
+ } else if (profile == BluetoothProfile.VOLUME_CONTROL) {
+ BluetoothVolumeControl vcs = new BluetoothVolumeControl(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.CSIP_SET_COORDINATOR) {
+ BluetoothCsipSetCoordinator csipSetCoordinator =
+ new BluetoothCsipSetCoordinator(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.LE_CALL_CONTROL) {
+ BluetoothLeCallControl tbs = new BluetoothLeCallControl(context, listener);
+ return true;
+ } else if (profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
+ BluetoothLeBroadcastAssistant leAudioBroadcastAssistant =
+ new BluetoothLeBroadcastAssistant(context, listener);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Close the connection of the profile proxy to the Service.
+ *
+ * <p>Clients should call this when they are no longer using the proxy obtained from {@link
+ * #getProfileProxy}. Profile can be one of {@link BluetoothProfile#HEADSET} or {@link
+ * BluetoothProfile#A2DP}
+ *
+ * @param unusedProfile
+ * @param proxy Profile proxy object
+ */
+ @SuppressLint({"AndroidFrameworkRequiresPermission", "AndroidFrameworkBluetoothPermission"})
+ public void closeProfileProxy(int unusedProfile, BluetoothProfile proxy) {
+ if (proxy == null) {
+ return;
+ }
+ proxy.close();
+ }
+
+ private static final IBluetoothManagerCallback sManagerCallback =
+ new IBluetoothManagerCallback.Stub() {
+ public void onBluetoothServiceUp(IBluetooth bluetoothService) {
+ if (DBG) {
+ Log.d(TAG, "onBluetoothServiceUp: " + bluetoothService);
+ }
+
+ synchronized (sServiceLock) {
+ sService = bluetoothService;
+ for (IBluetoothManagerCallback cb : sProxyServiceStateCallbacks.keySet()) {
+ try {
+ if (cb != null) {
+ cb.onBluetoothServiceUp(bluetoothService);
+ } else {
+ Log.d(TAG, "onBluetoothServiceUp: cb is null!");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+ }
+
+ public void onBluetoothServiceDown() {
+ if (DBG) {
+ Log.d(TAG, "onBluetoothServiceDown");
+ }
+
+ synchronized (sServiceLock) {
+ sService = null;
+ for (IBluetoothManagerCallback cb : sProxyServiceStateCallbacks.keySet()) {
+ try {
+ if (cb != null) {
+ cb.onBluetoothServiceDown();
+ } else {
+ Log.d(TAG, "onBluetoothServiceDown: cb is null!");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+ }
+
+ public void onBrEdrDown() {
+ if (VDBG) {
+ Log.i(TAG, "onBrEdrDown");
+ }
+
+ synchronized (sServiceLock) {
+ for (IBluetoothManagerCallback cb : sProxyServiceStateCallbacks.keySet()) {
+ try {
+ if (cb != null) {
+ cb.onBrEdrDown();
+ } else {
+ Log.d(TAG, "onBrEdrDown: cb is null!");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+ }
+ };
+
+ private final IBluetoothManagerCallback mManagerCallback =
+ new IBluetoothManagerCallback.Stub() {
+ public void onBluetoothServiceUp(@NonNull IBluetooth bluetoothService) {
+ requireNonNull(bluetoothService, "bluetoothService cannot be null");
+ mServiceLock.writeLock().lock();
+ try {
+ mService = bluetoothService;
+ } finally {
+ // lock downgrade is possible in ReentrantReadWriteLock
+ mServiceLock.readLock().lock();
+ mServiceLock.writeLock().unlock();
+ }
+ try {
+ synchronized (mMetadataListeners) {
+ mMetadataListeners.forEach((device, pair) -> {
+ try {
+ final SynchronousResultReceiver recv =
+ SynchronousResultReceiver.get();
+ mService.registerMetadataListener(mBluetoothMetadataListener,
+ device, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "Failed to register metadata listener", e);
+ Log.e(TAG, e.toString() + "\n"
+ + Log.getStackTraceString(new Throwable()));
+ }
+ });
+ }
+ synchronized (mAudioProfilesChangedCallbackExecutorMap) {
+ if (!mAudioProfilesChangedCallbackExecutorMap.isEmpty()) {
+ try {
+ final SynchronousResultReceiver recv =
+ SynchronousResultReceiver.get();
+ mService.registerPreferredAudioProfilesChangedCallback(
+ mPreferredAudioProfilesChangedCallback,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(
+ BluetoothStatusCodes.ERROR_UNKNOWN);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "onBluetoothServiceUp: Failed to register bluetooth"
+ + "connection callback", e);
+ }
+ }
+ }
+ synchronized (mBluetoothQualityReportReadyCallbackExecutorMap) {
+ if (!mBluetoothQualityReportReadyCallbackExecutorMap.isEmpty()) {
+ try {
+ final SynchronousResultReceiver recv =
+ SynchronousResultReceiver.get();
+ mService.registerBluetoothQualityReportReadyCallback(
+ mBluetoothQualityReportReadyCallback,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(
+ BluetoothStatusCodes.ERROR_UNKNOWN);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "onBluetoothServiceUp: Failed to register bluetooth"
+ + "quality report callback", e);
+ }
+ }
+ }
+ synchronized (mBluetoothConnectionCallbackExecutorMap) {
+ if (!mBluetoothConnectionCallbackExecutorMap.isEmpty()) {
+ try {
+ final SynchronousResultReceiver recv =
+ SynchronousResultReceiver.get();
+ mService.registerBluetoothConnectionCallback(
+ mConnectionCallback,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "onBluetoothServiceUp: Failed to register "
+ + "bluetooth connection callback", e);
+ }
+ }
+ }
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ public void onBluetoothServiceDown() {
+ mServiceLock.writeLock().lock();
+ try {
+ mService = null;
+ if (mLeScanClients != null) {
+ mLeScanClients.clear();
+ }
+ if (mBluetoothLeAdvertiser != null) {
+ mBluetoothLeAdvertiser.cleanup();
+ }
+ if (mBluetoothLeScanner != null) {
+ mBluetoothLeScanner.cleanup();
+ }
+ } finally {
+ mServiceLock.writeLock().unlock();
+ }
+ }
+
+ public void onBrEdrDown() {
+ }
+ };
+
+ /**
+ * Enable the Bluetooth Adapter, but don't auto-connect devices
+ * and don't persist state. Only for use by system applications.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean enableNoAutoConnect() {
+ if (isEnabled()) {
+ if (DBG) {
+ Log.d(TAG, "enableNoAutoConnect(): BT already enabled!");
+ }
+ return true;
+ }
+ try {
+ return mManagerService.enableNoAutoConnect(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_ANOTHER_ACTIVE_OOB_REQUEST,
+ })
+ public @interface OobError {}
+
+ /**
+ * Provides callback methods for receiving {@link OobData} from the host stack, as well as an
+ * error interface in order to allow the caller to determine next steps based on the {@code
+ * ErrorCode}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface OobDataCallback {
+ /**
+ * Handles the {@link OobData} received from the host stack.
+ *
+ * @param transport - whether the {@link OobData} is generated for LE or Classic.
+ * @param oobData - data generated in the host stack(LE) or controller (Classic)
+ */
+ void onOobData(@Transport int transport, @NonNull OobData oobData);
+
+ /**
+ * Provides feedback when things don't go as expected.
+ *
+ * @param errorCode - the code describing the type of error that occurred.
+ */
+ void onError(@OobError int errorCode);
+ }
+
+ /**
+ * Wraps an AIDL interface around an {@link OobDataCallback} interface.
+ *
+ * @see {@link IBluetoothOobDataCallback} for interface definition.
+ *
+ * @hide
+ */
+ public class WrappedOobDataCallback extends IBluetoothOobDataCallback.Stub {
+ private final OobDataCallback mCallback;
+ private final Executor mExecutor;
+
+ /**
+ * @param callback - object to receive {@link OobData} must be a non null argument
+ *
+ * @throws NullPointerException if the callback is null.
+ */
+ WrappedOobDataCallback(@NonNull OobDataCallback callback,
+ @NonNull @CallbackExecutor Executor executor) {
+ requireNonNull(callback);
+ requireNonNull(executor);
+ mCallback = callback;
+ mExecutor = executor;
+ }
+ /**
+ * Wrapper function to relay to the {@link OobDataCallback#onOobData}
+ *
+ * @param transport - whether the {@link OobData} is generated for LE or Classic.
+ * @param oobData - data generated in the host stack(LE) or controller (Classic)
+ *
+ * @hide
+ */
+ public void onOobData(@Transport int transport, @NonNull OobData oobData) {
+ mExecutor.execute(new Runnable() {
+ public void run() {
+ mCallback.onOobData(transport, oobData);
+ }
+ });
+ }
+ /**
+ * Wrapper function to relay to the {@link OobDataCallback#onError}
+ *
+ * @param errorCode - the code descibing the type of error that occurred.
+ *
+ * @hide
+ */
+ public void onError(@OobError int errorCode) {
+ mExecutor.execute(new Runnable() {
+ public void run() {
+ mCallback.onError(errorCode);
+ }
+ });
+ }
+ }
+
+ /**
+ * Fetches a secret data value that can be used for a secure and simple pairing experience.
+ *
+ * <p>This is the Local Out of Band data the comes from the
+ *
+ * <p>This secret is the local Out of Band data. This data is used to securely and quickly
+ * pair two devices with minimal user interaction.
+ *
+ * <p>For example, this secret can be transferred to a remote device out of band (meaning any
+ * other way besides using bluetooth). Once the remote device finds this device using the
+ * information given in the data, such as the PUBLIC ADDRESS, the remote device could then
+ * connect to this device using this secret when the pairing sequenece asks for the secret.
+ * This device will respond by automatically accepting the pairing due to the secret being so
+ * trustworthy.
+ *
+ * @param transport - provide type of transport (e.g. LE or Classic).
+ * @param callback - target object to receive the {@link OobData} value.
+ *
+ * @throws NullPointerException if callback is null.
+ * @throws IllegalArgumentException if the transport is not valid.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void generateLocalOobData(@Transport int transport,
+ @NonNull @CallbackExecutor Executor executor, @NonNull OobDataCallback callback) {
+ if (transport != BluetoothDevice.TRANSPORT_BREDR && transport
+ != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Invalid transport '" + transport + "'!");
+ }
+ requireNonNull(callback);
+ if (!isEnabled()) {
+ Log.w(TAG, "generateLocalOobData(): Adapter isn't enabled!");
+ callback.onError(BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED);
+ } else {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.generateLocalOobData(transport, new WrappedOobDataCallback(callback,
+ executor), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+ }
+
+ /**
+ * Enable control of the Bluetooth Adapter for a single application.
+ *
+ * <p>Some applications need to use Bluetooth for short periods of time to
+ * transfer data but don't want all the associated implications like
+ * automatic connection to headsets etc.
+ *
+ * <p> Multiple applications can call this. This is reference counted and
+ * Bluetooth disabled only when no one else is using it. There will be no UI
+ * shown to the user while bluetooth is being enabled. Any user action will
+ * override this call. For example, if user wants Bluetooth on and the last
+ * user of this API wanted to disable Bluetooth, Bluetooth will not be
+ * turned off.
+ *
+ * <p> This API is only meant to be used by internal applications. Third
+ * party applications but use {@link #enable} and {@link #disable} APIs.
+ *
+ * <p> If this API returns true, it means the callback will be called.
+ * The callback will be called with the current state of Bluetooth.
+ * If the state is not what was requested, an internal error would be the
+ * reason. If Bluetooth is already on and if this function is called to turn
+ * it on, the api will return true and a callback will be called.
+ *
+ * @param on True for on, false for off.
+ * @param callback The callback to notify changes to the state.
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean changeApplicationBluetoothState(boolean on,
+ BluetoothStateChangeCallback callback) {
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public interface BluetoothStateChangeCallback {
+ /**
+ * @hide
+ */
+ void onBluetoothStateChange(boolean on);
+ }
+
+ /**
+ * @hide
+ */
+ public class StateChangeCallbackWrapper extends IBluetoothStateChangeCallback.Stub {
+ private BluetoothStateChangeCallback mCallback;
+
+ StateChangeCallbackWrapper(BluetoothStateChangeCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onBluetoothStateChange(boolean on) {
+ mCallback.onBluetoothStateChange(on);
+ }
+ }
+
+ private Set<BluetoothDevice> toDeviceSet(List<BluetoothDevice> devices) {
+ Set<BluetoothDevice> deviceSet = new HashSet<BluetoothDevice>(devices);
+ return Collections.unmodifiableSet(deviceSet);
+ }
+
+ @SuppressLint("GenericException")
+ protected void finalize() throws Throwable {
+ try {
+ removeServiceStateCallback(mManagerCallback);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Validate a String Bluetooth address, such as "00:43:A8:23:10:F0"
+ * <p>Alphabetic characters must be uppercase to be valid.
+ *
+ * @param address Bluetooth address as string
+ * @return true if the address is valid, false otherwise
+ */
+ public static boolean checkBluetoothAddress(String address) {
+ if (address == null || address.length() != ADDRESS_LENGTH) {
+ return false;
+ }
+ for (int i = 0; i < ADDRESS_LENGTH; i++) {
+ char c = address.charAt(i);
+ switch (i % 3) {
+ case 0:
+ case 1:
+ if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')) {
+ // hex character, OK
+ break;
+ }
+ return false;
+ case 2:
+ if (c == ':') {
+ break; // OK
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Determines whether a String Bluetooth address, such as "F0:43:A8:23:10:00"
+ * is a RANDOM STATIC address.
+ *
+ * RANDOM STATIC: (addr & 0xC0) == 0xC0
+ * RANDOM RESOLVABLE: (addr & 0xC0) == 0x40
+ * RANDOM non-RESOLVABLE: (addr & 0xC0) == 0x00
+ *
+ * @param address Bluetooth address as string
+ * @return true if the 2 Most Significant Bits of the address equals 0xC0.
+ *
+ * @hide
+ */
+ public static boolean isAddressRandomStatic(@NonNull String address) {
+ requireNonNull(address);
+ return checkBluetoothAddress(address)
+ && (Integer.parseInt(address.split(":")[0], 16) & 0xC0) == 0xC0;
+ }
+
+ /** {@hide} */
+ @UnsupportedAppUsage
+ @RequiresNoPermission
+ public IBluetoothManager getBluetoothManager() {
+ return mManagerService;
+ }
+
+ /** {@hide} */
+ @RequiresNoPermission
+ public AttributionSource getAttributionSource() {
+ return mAttributionSource;
+ }
+
+ @GuardedBy("sServiceLock")
+ private static final WeakHashMap<IBluetoothManagerCallback, Void> sProxyServiceStateCallbacks =
+ new WeakHashMap<>();
+
+ /*package*/ IBluetooth getBluetoothService() {
+ synchronized (sServiceLock) {
+ return sService;
+ }
+ }
+
+ /**
+ * Registers a IBluetoothManagerCallback and returns the cached
+ * Bluetooth service proxy object.
+ *
+ * TODO: rename this API to registerBlueoothManagerCallback or something?
+ * the current name does not match what it does very well.
+ *
+ * /
+ @UnsupportedAppUsage
+ /*package*/ IBluetooth getBluetoothService(IBluetoothManagerCallback cb) {
+ requireNonNull(cb);
+ synchronized (sServiceLock) {
+ sProxyServiceStateCallbacks.put(cb, null);
+ registerOrUnregisterAdapterLocked();
+ return sService;
+ }
+ }
+
+ /*package*/ void removeServiceStateCallback(IBluetoothManagerCallback cb) {
+ requireNonNull(cb);
+ synchronized (sServiceLock) {
+ sProxyServiceStateCallbacks.remove(cb);
+ registerOrUnregisterAdapterLocked();
+ }
+ }
+
+ /**
+ * Handle registering (or unregistering) a single process-wide
+ * {@link IBluetoothManagerCallback} based on the presence of local
+ * {@link #sProxyServiceStateCallbacks} clients.
+ */
+ @GuardedBy("sServiceLock")
+ private void registerOrUnregisterAdapterLocked() {
+ final boolean isRegistered = sServiceRegistered;
+ final boolean wantRegistered = !sProxyServiceStateCallbacks.isEmpty();
+
+ if (isRegistered != wantRegistered) {
+ if (wantRegistered) {
+ try {
+ sService = mManagerService.registerAdapter(sManagerCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ try {
+ mManagerService.unregisterAdapter(sManagerCallback);
+ sService = null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ sServiceRegistered = wantRegistered;
+ }
+ }
+
+ /**
+ * Callback interface used to deliver LE scan results.
+ *
+ * @see #startLeScan(LeScanCallback)
+ * @see #startLeScan(UUID[], LeScanCallback)
+ */
+ public interface LeScanCallback {
+ /**
+ * Callback reporting an LE device found during a device scan initiated
+ * by the {@link BluetoothAdapter#startLeScan} function.
+ *
+ * @param device Identifies the remote device
+ * @param rssi The RSSI value for the remote device as reported by the Bluetooth hardware. 0
+ * if no RSSI value is available.
+ * @param scanRecord The content of the advertisement record offered by the remote device.
+ */
+ void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord);
+ }
+
+ /**
+ * Register a callback to receive events whenever the bluetooth stack goes down and back up,
+ * e.g. in the event the bluetooth is turned off/on via settings.
+ *
+ * If the bluetooth stack is currently up, there will not be an initial callback call.
+ * You can use the return value as an indication of this being the case.
+ *
+ * Callbacks will be delivered on a binder thread.
+ *
+ * @return whether bluetooth is already up currently
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public boolean registerServiceLifecycleCallback(@NonNull ServiceLifecycleCallback callback) {
+ return getBluetoothService(callback.mRemote) != null;
+ }
+
+ /**
+ * Unregister a callback registered via {@link #registerServiceLifecycleCallback}
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public void unregisterServiceLifecycleCallback(@NonNull ServiceLifecycleCallback callback) {
+ removeServiceStateCallback(callback.mRemote);
+ }
+
+ /**
+ * A callback for {@link #registerServiceLifecycleCallback}
+ *
+ * @hide
+ */
+ public abstract static class ServiceLifecycleCallback {
+
+ /** Called when the bluetooth stack is up */
+ public abstract void onBluetoothServiceUp();
+
+ /** Called when the bluetooth stack is down */
+ public abstract void onBluetoothServiceDown();
+
+ IBluetoothManagerCallback mRemote = new IBluetoothManagerCallback.Stub() {
+ @Override
+ public void onBluetoothServiceUp(IBluetooth bluetoothService) {
+ ServiceLifecycleCallback.this.onBluetoothServiceUp();
+ }
+
+ @Override
+ public void onBluetoothServiceDown() {
+ ServiceLifecycleCallback.this.onBluetoothServiceDown();
+ }
+
+ @Override
+ public void onBrEdrDown() {}
+ };
+ }
+
+ /**
+ * Starts a scan for Bluetooth LE devices.
+ *
+ * <p>Results of the scan are reported using the
+ * {@link LeScanCallback#onLeScan} callback.
+ *
+ * @param callback the callback LE scan results are delivered
+ * @return true, if the scan was started successfully
+ * @deprecated use {@link BluetoothLeScanner#startScan(List, ScanSettings, ScanCallback)}
+ * instead.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean startLeScan(LeScanCallback callback) {
+ return startLeScan(null, callback);
+ }
+
+ /**
+ * Starts a scan for Bluetooth LE devices, looking for devices that
+ * advertise given services.
+ *
+ * <p>Devices which advertise all specified services are reported using the
+ * {@link LeScanCallback#onLeScan} callback.
+ *
+ * @param serviceUuids Array of services to look for
+ * @param callback the callback LE scan results are delivered
+ * @return true, if the scan was started successfully
+ * @deprecated use {@link BluetoothLeScanner#startScan(List, ScanSettings, ScanCallback)}
+ * instead.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean startLeScan(final UUID[] serviceUuids, final LeScanCallback callback) {
+ if (DBG) {
+ Log.d(TAG, "startLeScan(): " + Arrays.toString(serviceUuids));
+ }
+ if (callback == null) {
+ if (DBG) {
+ Log.e(TAG, "startLeScan: null callback");
+ }
+ return false;
+ }
+ BluetoothLeScanner scanner = getBluetoothLeScanner();
+ if (scanner == null) {
+ if (DBG) {
+ Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner");
+ }
+ return false;
+ }
+
+ synchronized (mLeScanClients) {
+ if (mLeScanClients.containsKey(callback)) {
+ if (DBG) {
+ Log.e(TAG, "LE Scan has already started");
+ }
+ return false;
+ }
+
+ try {
+ IBluetoothGatt iGatt = mManagerService.getBluetoothGatt();
+ if (iGatt == null) {
+ // BLE is not supported
+ return false;
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ ScanCallback scanCallback = new ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ if (callbackType != ScanSettings.CALLBACK_TYPE_ALL_MATCHES) {
+ // Should not happen.
+ Log.e(TAG, "LE Scan has already started");
+ return;
+ }
+ ScanRecord scanRecord = result.getScanRecord();
+ if (scanRecord == null) {
+ return;
+ }
+ if (serviceUuids != null) {
+ List<ParcelUuid> uuids = new ArrayList<ParcelUuid>();
+ for (UUID uuid : serviceUuids) {
+ uuids.add(new ParcelUuid(uuid));
+ }
+ List<ParcelUuid> scanServiceUuids = scanRecord.getServiceUuids();
+ if (scanServiceUuids == null || !scanServiceUuids.containsAll(uuids)) {
+ if (DBG) {
+ Log.d(TAG, "uuids does not match");
+ }
+ return;
+ }
+ }
+ callback.onLeScan(result.getDevice(), result.getRssi(),
+ scanRecord.getBytes());
+ }
+ };
+ ScanSettings settings = new ScanSettings.Builder().setCallbackType(
+ ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build();
+
+ List<ScanFilter> filters = new ArrayList<ScanFilter>();
+ if (serviceUuids != null && serviceUuids.length > 0) {
+ // Note scan filter does not support matching an UUID array so we put one
+ // UUID to hardware and match the whole array in callback.
+ ScanFilter filter =
+ new ScanFilter.Builder().setServiceUuid(new ParcelUuid(serviceUuids[0]))
+ .build();
+ filters.add(filter);
+ }
+ scanner.startScan(filters, settings, scanCallback);
+
+ mLeScanClients.put(callback, scanCallback);
+ return true;
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Stops an ongoing Bluetooth LE device scan.
+ *
+ * @param callback used to identify which scan to stop must be the same handle used to start the
+ * scan
+ * @deprecated Use {@link BluetoothLeScanner#stopScan(ScanCallback)} instead.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void stopLeScan(LeScanCallback callback) {
+ if (DBG) {
+ Log.d(TAG, "stopLeScan()");
+ }
+ BluetoothLeScanner scanner = getBluetoothLeScanner();
+ if (scanner == null) {
+ return;
+ }
+ synchronized (mLeScanClients) {
+ ScanCallback scanCallback = mLeScanClients.remove(callback);
+ if (scanCallback == null) {
+ if (DBG) {
+ Log.d(TAG, "scan not started yet");
+ }
+ return;
+ }
+ scanner.stopScan(scanCallback);
+ }
+ }
+
+ /**
+ * Create a secure L2CAP Connection-oriented Channel (CoC) {@link BluetoothServerSocket} and
+ * assign a dynamic protocol/service multiplexer (PSM) value. This socket can be used to listen
+ * for incoming connections. The supported Bluetooth transport is LE only.
+ * <p>A remote device connecting to this socket will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming connections from a listening
+ * {@link BluetoothServerSocket}.
+ * <p>The system will assign a dynamic PSM value. This PSM value can be read from the {@link
+ * BluetoothServerSocket#getPsm()} and this value will be released when this server socket is
+ * closed, Bluetooth is turned off, or the application exits unexpectedly.
+ * <p>The mechanism of disclosing the assigned dynamic PSM value to the initiating peer is
+ * defined and performed by the application.
+ * <p>Use {@link BluetoothDevice#createL2capChannel(int)} to connect to this server
+ * socket from another Android device that is given the PSM value.
+ *
+ * @return an L2CAP CoC BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or unable to start this CoC
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull BluetoothServerSocket listenUsingL2capChannel()
+ throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP_LE, true, true,
+ SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false, false);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ throw new IOException("Error: " + errno);
+ }
+
+ int assignedPsm = socket.mSocket.getPort();
+ if (assignedPsm == 0) {
+ throw new IOException("Error: Unable to assign PSM value");
+ }
+ if (DBG) {
+ Log.d(TAG, "listenUsingL2capChannel: set assigned PSM to "
+ + assignedPsm);
+ }
+ socket.setChannel(assignedPsm);
+
+ return socket;
+ }
+
+ /**
+ * Create an insecure L2CAP Connection-oriented Channel (CoC) {@link BluetoothServerSocket} and
+ * assign a dynamic PSM value. This socket can be used to listen for incoming connections. The
+ * supported Bluetooth transport is LE only.
+ * <p>The link key is not required to be authenticated, i.e the communication may be vulnerable
+ * to person-in-the-middle attacks. Use {@link #listenUsingL2capChannel}, if an encrypted and
+ * authenticated communication channel is desired.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming connections from a listening
+ * {@link BluetoothServerSocket}.
+ * <p>The system will assign a dynamic protocol/service multiplexer (PSM) value. This PSM value
+ * can be read from the {@link BluetoothServerSocket#getPsm()} and this value will be released
+ * when this server socket is closed, Bluetooth is turned off, or the application exits
+ * unexpectedly.
+ * <p>The mechanism of disclosing the assigned dynamic PSM value to the initiating peer is
+ * defined and performed by the application.
+ * <p>Use {@link BluetoothDevice#createInsecureL2capChannel(int)} to connect to this server
+ * socket from another Android device that is given the PSM value.
+ *
+ * @return an L2CAP CoC BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or unable to start this CoC
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull BluetoothServerSocket listenUsingInsecureL2capChannel()
+ throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP_LE, false, false,
+ SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false, false);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ throw new IOException("Error: " + errno);
+ }
+
+ int assignedPsm = socket.mSocket.getPort();
+ if (assignedPsm == 0) {
+ throw new IOException("Error: Unable to assign PSM value");
+ }
+ if (DBG) {
+ Log.d(TAG, "listenUsingInsecureL2capChannel: set assigned PSM to "
+ + assignedPsm);
+ }
+ socket.setChannel(assignedPsm);
+
+ return socket;
+ }
+
+ /**
+ * Register a {@link #OnMetadataChangedListener} to receive update about metadata
+ * changes for this {@link BluetoothDevice}.
+ * Registration must be done when Bluetooth is ON and will last until
+ * {@link #removeOnMetadataChangedListener(BluetoothDevice)} is called, even when Bluetooth
+ * restarted in the middle.
+ * All input parameters should not be null or {@link NullPointerException} will be triggered.
+ * The same {@link BluetoothDevice} and {@link #OnMetadataChangedListener} pair can only be
+ * registered once, double registration would cause {@link IllegalArgumentException}.
+ *
+ * @param device {@link BluetoothDevice} that will be registered
+ * @param executor the executor for listener callback
+ * @param listener {@link #OnMetadataChangedListener} that will receive asynchronous callbacks
+ * @return true on success, false on error
+ * @throws NullPointerException If one of {@code listener}, {@code device} or {@code executor}
+ * is null.
+ * @throws IllegalArgumentException The same {@link #OnMetadataChangedListener} and
+ * {@link BluetoothDevice} are registered twice.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean addOnMetadataChangedListener(@NonNull BluetoothDevice device,
+ @NonNull Executor executor, @NonNull OnMetadataChangedListener listener) {
+ if (DBG) Log.d(TAG, "addOnMetadataChangedListener()");
+
+ if (listener == null) {
+ throw new NullPointerException("listener is null");
+ }
+ if (device == null) {
+ throw new NullPointerException("device is null");
+ }
+ if (executor == null) {
+ throw new NullPointerException("executor is null");
+ }
+
+ mServiceLock.readLock().lock();
+ try {
+ if (mService == null) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot register metadata listener");
+ return false;
+ }
+
+
+ synchronized (mMetadataListeners) {
+ List<Pair<OnMetadataChangedListener, Executor>> listenerList =
+ mMetadataListeners.get(device);
+ if (listenerList == null) {
+ // Create new listener/executor list for registration
+ listenerList = new ArrayList<>();
+ mMetadataListeners.put(device, listenerList);
+ } else {
+ // Check whether this device is already registered by the listener
+ if (listenerList.stream().anyMatch((pair) -> (pair.first.equals(listener)))) {
+ throw new IllegalArgumentException("listener was already regestered"
+ + " for the device");
+ }
+ }
+
+ Pair<OnMetadataChangedListener, Executor> listenerPair =
+ new Pair(listener, executor);
+ listenerList.add(listenerPair);
+
+ boolean ret = false;
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.registerMetadataListener(mBluetoothMetadataListener, device,
+ mAttributionSource, recv);
+ ret = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ if (!ret) {
+ // Remove listener registered earlier when fail.
+ listenerList.remove(listenerPair);
+ if (listenerList.isEmpty()) {
+ // Remove the device if its listener list is empty
+ mMetadataListeners.remove(device);
+ }
+ }
+ }
+ return ret;
+ }
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Unregister a {@link #OnMetadataChangedListener} from a registered {@link BluetoothDevice}.
+ * Unregistration can be done when Bluetooth is either ON or OFF.
+ * {@link #addOnMetadataChangedListener(OnMetadataChangedListener, BluetoothDevice, Executor)}
+ * must be called before unregisteration.
+ *
+ * @param device {@link BluetoothDevice} that will be unregistered. It
+ * should not be null or {@link NullPointerException} will be triggered.
+ * @param listener {@link OnMetadataChangedListener} that will be unregistered. It
+ * should not be null or {@link NullPointerException} will be triggered.
+ * @return true on success, false on error
+ * @throws NullPointerException If {@code listener} or {@code device} is null.
+ * @throws IllegalArgumentException If {@code device} has not been registered before.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean removeOnMetadataChangedListener(@NonNull BluetoothDevice device,
+ @NonNull OnMetadataChangedListener listener) {
+ if (DBG) Log.d(TAG, "removeOnMetadataChangedListener()");
+ if (device == null) {
+ throw new NullPointerException("device is null");
+ }
+ if (listener == null) {
+ throw new NullPointerException("listener is null");
+ }
+
+ synchronized (mMetadataListeners) {
+ if (!mMetadataListeners.containsKey(device)) {
+ throw new IllegalArgumentException("device was not registered");
+ }
+ // Remove issued listener from the registered device
+ mMetadataListeners.get(device).removeIf((pair) -> (pair.first.equals(listener)));
+
+ if (mMetadataListeners.get(device).isEmpty()) {
+ // Unregister to Bluetooth service if all listeners are removed from
+ // the registered device
+ mMetadataListeners.remove(device);
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv =
+ SynchronousResultReceiver.get();
+ mService.unregisterMetadataListener(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ return false;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ }
+ }
+ return true;
+ }
+
+ /**
+ * This interface is used to implement {@link BluetoothAdapter} metadata listener.
+ * @hide
+ */
+ @SystemApi
+ public interface OnMetadataChangedListener {
+ /**
+ * Callback triggered if the metadata of {@link BluetoothDevice} registered in
+ * {@link #addOnMetadataChangedListener}.
+ *
+ * @param device changed {@link BluetoothDevice}.
+ * @param key changed metadata key, one of BluetoothDevice.METADATA_*.
+ * @param value the new value of metadata as byte array.
+ */
+ void onMetadataChanged(@NonNull BluetoothDevice device, int key,
+ @Nullable byte[] value);
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothConnectionCallback mConnectionCallback =
+ new IBluetoothConnectionCallback.Stub() {
+ @Override
+ public void onDeviceConnected(BluetoothDevice device) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ for (Map.Entry<BluetoothConnectionCallback, Executor> callbackExecutorEntry:
+ mBluetoothConnectionCallbackExecutorMap.entrySet()) {
+ BluetoothConnectionCallback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onDeviceConnected(device));
+ }
+ }
+
+ @Override
+ public void onDeviceDisconnected(BluetoothDevice device, int hciReason) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ for (Map.Entry<BluetoothConnectionCallback, Executor> callbackExecutorEntry:
+ mBluetoothConnectionCallbackExecutorMap.entrySet()) {
+ BluetoothConnectionCallback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onDeviceDisconnected(device, hciReason));
+ }
+ }
+ };
+
+ /**
+ * Registers the BluetoothConnectionCallback to receive callback events when a bluetooth device
+ * (classic or low energy) is connected or disconnected.
+ *
+ * @param executor is the callback executor
+ * @param callback is the connection callback you wish to register
+ * @return true if the callback was registered successfully, false otherwise
+ * @throws IllegalArgumentException if the callback is already registered
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean registerBluetoothConnectionCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull BluetoothConnectionCallback callback) {
+ if (DBG) Log.d(TAG, "registerBluetoothConnectionCallback()");
+ if (callback == null || executor == null) {
+ return false;
+ }
+
+ synchronized (mBluetoothConnectionCallbackExecutorMap) {
+ // If the callback map is empty, we register the service-to-app callback
+ if (mBluetoothConnectionCallbackExecutorMap.isEmpty()) {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv =
+ SynchronousResultReceiver.get();
+ mService.registerBluetoothConnectionCallback(mConnectionCallback,
+ mAttributionSource, recv);
+ if (!recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false)) {
+ return false;
+ }
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ mBluetoothConnectionCallbackExecutorMap.remove(callback);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ // Adds the passed in callback to our map of callbacks to executors
+ if (mBluetoothConnectionCallbackExecutorMap.containsKey(callback)) {
+ throw new IllegalArgumentException("This callback has already been registered");
+ }
+ mBluetoothConnectionCallbackExecutorMap.put(callback, executor);
+ }
+
+ return true;
+ }
+
+ /**
+ * Unregisters the BluetoothConnectionCallback that was previously registered by the application
+ *
+ * @param callback is the connection callback you wish to unregister
+ * @return true if the callback was unregistered successfully, false otherwise
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean unregisterBluetoothConnectionCallback(
+ @NonNull BluetoothConnectionCallback callback) {
+ if (DBG) Log.d(TAG, "unregisterBluetoothConnectionCallback()");
+ if (callback == null) {
+ return false;
+ }
+
+ synchronized (mBluetoothConnectionCallbackExecutorMap) {
+ if (mBluetoothConnectionCallbackExecutorMap.remove(callback) != null) {
+ return false;
+ }
+ }
+
+ if (!mBluetoothConnectionCallbackExecutorMap.isEmpty()) {
+ return true;
+ }
+
+ // If the callback map is empty, we unregister the service-to-app callback
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ mService.unregisterBluetoothConnectionCallback(mConnectionCallback,
+ mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return false;
+ }
+
+ /**
+ * This abstract class is used to implement callbacks for when a bluetooth classic or Bluetooth
+ * Low Energy (BLE) device is either connected or disconnected.
+ *
+ * @hide
+ */
+ @SystemApi
+ public abstract static class BluetoothConnectionCallback {
+ /**
+ * Callback triggered when a bluetooth device (classic or BLE) is connected
+ * @param device is the connected bluetooth device
+ */
+ public void onDeviceConnected(@NonNull BluetoothDevice device) {}
+
+ /**
+ * Callback triggered when a bluetooth device (classic or BLE) is disconnected
+ * @param device is the disconnected bluetooth device
+ * @param reason is the disconnect reason
+ */
+ public void onDeviceDisconnected(@NonNull BluetoothDevice device,
+ @DisconnectReason int reason) {}
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "REASON_" }, value = {
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_LOCAL_REQUEST,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_REMOTE_REQUEST,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_LOCAL,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_REMOTE,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_TIMEOUT,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_SECURITY,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_SYSTEM_POLICY,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_RESOURCE_LIMIT_REACHED,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_CONNECTION_ALREADY_EXISTS,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_BAD_PARAMETERS})
+ public @interface DisconnectReason {}
+
+ /**
+ * Returns human-readable strings corresponding to {@link DisconnectReason}.
+ */
+ @NonNull
+ public static String disconnectReasonToString(@DisconnectReason int reason) {
+ switch (reason) {
+ case BluetoothStatusCodes.ERROR_UNKNOWN:
+ return "Reason unknown";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_LOCAL_REQUEST:
+ return "Local request";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_REMOTE_REQUEST:
+ return "Remote request";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_LOCAL:
+ return "Local error";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_REMOTE:
+ return "Remote error";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_TIMEOUT:
+ return "Timeout";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_SECURITY:
+ return "Security";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_SYSTEM_POLICY:
+ return "System policy";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_RESOURCE_LIMIT_REACHED:
+ return "Resource constrained";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_CONNECTION_ALREADY_EXISTS:
+ return "Connection already exists";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_BAD_PARAMETERS:
+ return "Bad parameters";
+ default:
+ return "Unrecognized disconnect reason: " + reason;
+ }
+ }
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_ANOTHER_ACTIVE_REQUEST,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_NOT_DUAL_MODE_AUDIO_DEVICE,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED,
+ })
+ public @interface SetPreferredAudioProfilesReturnValues {}
+
+ /**
+ * Sets the preferred profiles for each audio mode for system routed audio. The audio framework
+ * and Telecomm will read this preference when routing system managed audio. Not supplying an
+ * audio mode in the Bundle will reset that audio mode to the default profile preference for
+ * that mode (e.g. an empty Bundle resets all audio modes to their default profiles).
+ * <p>
+ * Note: apps that invoke profile-specific audio APIs are not subject to the preference noted
+ * here. These preferences will also be ignored if the remote device is not simultaneously
+ * connected to a classic audio profile (A2DP and/or HFP) and LE Audio at the same time. If the
+ * remote device does not support both BR/EDR audio and LE Audio, this API returns
+ * {@link BluetoothStatusCodes#ERROR_NOT_DUAL_MODE_AUDIO_DEVICE}. If the system property
+ * persist.bluetooth.enable_dual_mode_audio is set to {@code false}, this API returns
+ * {@link BluetoothStatusCodes#FEATURE_NOT_SUPPORTED}.
+ * <p>
+ * The Bundle is expected to contain the following mappings:
+ * 1. For key {@link #AUDIO_MODE_OUTPUT_ONLY}, it expects an integer value of either
+ * {@link BluetoothProfile#A2DP} or {@link BluetoothProfile#LE_AUDIO}.
+ * 2. For key {@link #AUDIO_MODE_DUPLEX}, it expects an integer value of either
+ * {@link BluetoothProfile#HEADSET} or {@link BluetoothProfile#LE_AUDIO}.
+ * <p>
+ * Apps should register for a callback with
+ * {@link #registerPreferredAudioProfilesChangedCallback(Executor,
+ * PreferredAudioProfilesChangedCallback)} to know if the preferences were successfully applied
+ * to the audio framework. If there is an active preference change for this device that has not
+ * taken effect with the audio framework, no additional calls to this API will be allowed until
+ * that completes.
+ *
+ * @param modeToProfileBundle a mapping to indicate the preferred profile for each audio mode
+ * @return whether the preferred audio profiles were requested to be set
+ * @throws NullPointerException if modeToProfileBundle or device is null
+ * @throws IllegalArgumentException if this BluetoothDevice object has an invalid address or the
+ * Bundle doesn't conform to its requirements
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SetPreferredAudioProfilesReturnValues
+ public int setPreferredAudioProfiles(@NonNull BluetoothDevice device,
+ @NonNull Bundle modeToProfileBundle) {
+ if (DBG) {
+ Log.d(TAG, "setPreferredAudioProfiles( " + modeToProfileBundle + ", " + device + ")");
+ }
+ requireNonNull(modeToProfileBundle, "modeToProfileBundle must not be null");
+ requireNonNull(device, "device must not be null");
+ if (!BluetoothAdapter.checkBluetoothAddress(getAddress())) {
+ throw new IllegalArgumentException("device cannot have an invalid address");
+ }
+ if (!modeToProfileBundle.containsKey(AUDIO_MODE_OUTPUT_ONLY)
+ && !modeToProfileBundle.containsKey(AUDIO_MODE_DUPLEX)) {
+ throw new IllegalArgumentException("Bundle does not contain a key "
+ + "AUDIO_MODE_OUTPUT_ONLY or AUDIO_MODE_DUPLEX");
+ }
+ if (modeToProfileBundle.containsKey(AUDIO_MODE_OUTPUT_ONLY)
+ && modeToProfileBundle.getInt(AUDIO_MODE_OUTPUT_ONLY) != BluetoothProfile.A2DP
+ && modeToProfileBundle.getInt(
+ AUDIO_MODE_OUTPUT_ONLY) != BluetoothProfile.LE_AUDIO) {
+ throw new IllegalArgumentException("Key AUDIO_MODE_OUTPUT_ONLY has an invalid value: "
+ + modeToProfileBundle.getInt(AUDIO_MODE_OUTPUT_ONLY));
+ }
+ if (modeToProfileBundle.containsKey(AUDIO_MODE_DUPLEX)
+ && modeToProfileBundle.getInt(AUDIO_MODE_DUPLEX) != BluetoothProfile.HEADSET
+ && modeToProfileBundle.getInt(AUDIO_MODE_DUPLEX) != BluetoothProfile.LE_AUDIO) {
+ throw new IllegalArgumentException("Key AUDIO_MODE_DUPLEX has an invalid value: "
+ + modeToProfileBundle.getInt(AUDIO_MODE_DUPLEX));
+ }
+
+ final int defaultValue = BluetoothStatusCodes.ERROR_UNKNOWN;
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.setPreferredAudioProfiles(device, modeToProfileBundle,
+ mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } else {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the preferred profile for each audio mode for system routed audio. This API
+ * returns a Bundle with mappings between each audio mode and its preferred audio profile. If no
+ * values are set via {@link #setPreferredAudioProfiles(BluetoothDevice, Bundle)}, this API
+ * returns the default system preferences set via the sysprops
+ * {@link BluetoothProperties#getDefaultOutputOnlyAudioProfile()} and
+ * {@link BluetoothProperties#getDefaultDuplexAudioProfile()}.
+ * <p>
+ * An audio capable device must support at least one audio mode with a preferred audio profile.
+ * If a device does not support an audio mode, the audio mode will be omitted from the keys of
+ * the Bundle. If the device is not recognized as a dual mode audio capable device (e.g. because
+ * it is not bonded, does not support any audio profiles, or does not support both BR/EDR audio
+ * and LE Audio), this API returns an empty Bundle. If the system property
+ * persist.bluetooth.enable_dual_mode_audio is set to {@code false}, this API returns an empty
+ * Bundle.
+ * <p>
+ * The Bundle can contain the following mappings:
+ * <ul>
+ * <li>For key {@link #AUDIO_MODE_OUTPUT_ONLY}, if an audio profile preference was set, this
+ * will have an int value of either {@link BluetoothProfile#A2DP} or
+ * {@link BluetoothProfile#LE_AUDIO}.
+ * <li>For key {@link #AUDIO_MODE_DUPLEX}, if an audio profile preference was set, this will
+ * have an int value of either {@link BluetoothProfile#HEADSET} or
+ * {@link BluetoothProfile#LE_AUDIO}.
+ * </ul>
+ *
+ * @return a Bundle mapping each set audio mode and preferred audio profile pair
+ * @throws NullPointerException if modeToProfileBundle or device is null
+ * @throws IllegalArgumentException if this BluetoothDevice object has an invalid address or the
+ * Bundle doesn't conform to its requirements
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @NonNull
+ public Bundle getPreferredAudioProfiles(@NonNull BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "getPreferredAudioProfiles(" + device + ")");
+ requireNonNull(device, "device cannot be null");
+ if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) {
+ throw new IllegalArgumentException("device cannot have an invalid address");
+ }
+
+ final Bundle defaultValue = Bundle.EMPTY;
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Bundle> recv = SynchronousResultReceiver.get();
+ mService.getPreferredAudioProfiles(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return defaultValue;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_UNKNOWN
+ })
+ public @interface NotifyActiveDeviceChangeAppliedReturnValues {}
+
+ /**
+ * Called by audio framework to inform the Bluetooth stack that an active device change has
+ * taken effect. If this active device change is triggered by an app calling
+ * {@link #setPreferredAudioProfiles(BluetoothDevice, Bundle)}, the Bluetooth stack will invoke
+ * {@link PreferredAudioProfilesChangedCallback#onPreferredAudioProfilesChanged(
+ * BluetoothDevice, Bundle, int)} if all requested changes for the device have been applied.
+ * <p>
+ * This method will return
+ * {@link BluetoothStatusCodes#ERROR_BLUETOOTH_NOT_ALLOWED} if called outside system server.
+ *
+ * @param device is the BluetoothDevice that had its preferred audio profile changed
+ * @return whether the Bluetooth stack acknowledged the change successfully
+ * @throws NullPointerException if device is null
+ * @throws IllegalArgumentException if the device's address is invalid
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @NotifyActiveDeviceChangeAppliedReturnValues
+ public int notifyActiveDeviceChangeApplied(@NonNull BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "notifyActiveDeviceChangeApplied(" + device + ")");
+ requireNonNull(device, "device cannot be null");
+ if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) {
+ throw new IllegalArgumentException("device cannot have an invalid address");
+ }
+
+ final int defaultValue = BluetoothStatusCodes.ERROR_UNKNOWN;
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.notifyActiveDeviceChangeApplied(device,
+ mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return defaultValue;
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothPreferredAudioProfilesCallback mPreferredAudioProfilesChangedCallback =
+ new IBluetoothPreferredAudioProfilesCallback.Stub() {
+ @Override
+ public void onPreferredAudioProfilesChanged(BluetoothDevice device,
+ Bundle preferredAudioProfiles, int status) {
+ for (Map.Entry<PreferredAudioProfilesChangedCallback, Executor>
+ callbackExecutorEntry:
+ mAudioProfilesChangedCallbackExecutorMap.entrySet()) {
+ PreferredAudioProfilesChangedCallback callback =
+ callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onPreferredAudioProfilesChanged(device,
+ preferredAudioProfiles, status));
+ }
+ }
+ };
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED
+ })
+ public @interface RegisterPreferredAudioProfilesCallbackReturnValues {}
+
+ /**
+ * Registers a callback to be notified when the preferred audio profile changes have taken
+ * effect. To unregister this callback, call
+ * {@link #unregisterPreferredAudioProfilesChangedCallback(
+ * PreferredAudioProfilesChangedCallback)}. If the system property
+ * persist.bluetooth.enable_dual_mode_audio is set to {@code false}, this API returns
+ * {@link BluetoothStatusCodes#FEATURE_NOT_SUPPORTED}.
+ *
+ * @param executor an {@link Executor} to execute the callbacks
+ * @param callback user implementation of the {@link PreferredAudioProfilesChangedCallback}
+ * @return whether the callback was registered successfully
+ * @throws NullPointerException if executor or callback is null
+ * @throws IllegalArgumentException if the callback is already registered
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @RegisterPreferredAudioProfilesCallbackReturnValues
+ public int registerPreferredAudioProfilesChangedCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull PreferredAudioProfilesChangedCallback callback) {
+ if (DBG) Log.d(TAG, "registerPreferredAudioProfilesChangedCallback()");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+
+ final int defaultValue = BluetoothStatusCodes.ERROR_UNKNOWN;
+ int serviceCallStatus = defaultValue;
+ synchronized (mAudioProfilesChangedCallbackExecutorMap) {
+ // If the callback map is empty, we register the service-to-app callback
+ if (mAudioProfilesChangedCallbackExecutorMap.isEmpty()) {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ mService.registerPreferredAudioProfilesChangedCallback(
+ mPreferredAudioProfilesChangedCallback, mAttributionSource, recv);
+ serviceCallStatus = recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(defaultValue);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ if (serviceCallStatus != BluetoothStatusCodes.SUCCESS) {
+ return serviceCallStatus;
+ }
+ }
+
+ // Adds the passed in callback to our local mapping
+ if (mAudioProfilesChangedCallbackExecutorMap.containsKey(callback)) {
+ throw new IllegalArgumentException("This callback has already been registered");
+ } else {
+ mAudioProfilesChangedCallbackExecutorMap.put(callback, executor);
+ }
+ }
+
+ return BluetoothStatusCodes.SUCCESS;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_CALLBACK_NOT_REGISTERED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED
+ })
+ public @interface UnRegisterPreferredAudioProfilesCallbackReturnValues {}
+
+ /**
+ * Unregisters a callback that was previously registered with
+ * {@link #registerPreferredAudioProfilesChangedCallback(Executor,
+ * PreferredAudioProfilesChangedCallback)}.
+ *
+ * @param callback user implementation of the {@link PreferredAudioProfilesChangedCallback}
+ * @return whether the callback was successfully unregistered
+ * @throws NullPointerException if the callback is null
+ * @throws IllegalArgumentException if the callback has not been registered
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @UnRegisterPreferredAudioProfilesCallbackReturnValues
+ public int unregisterPreferredAudioProfilesChangedCallback(
+ @NonNull PreferredAudioProfilesChangedCallback callback) {
+ if (DBG) Log.d(TAG, "unregisterPreferredAudioProfilesChangedCallback()");
+ requireNonNull(callback, "callback cannot be null");
+
+ synchronized (mAudioProfilesChangedCallbackExecutorMap) {
+ if (mAudioProfilesChangedCallbackExecutorMap.remove(callback) == null) {
+ throw new IllegalArgumentException("This callback has not been registered");
+ }
+ }
+
+ if (!mBluetoothConnectionCallbackExecutorMap.isEmpty()) {
+ return BluetoothStatusCodes.SUCCESS;
+ }
+
+ final int defaultValue = BluetoothStatusCodes.ERROR_UNKNOWN;
+ // If the callback map is empty, we unregister the service-to-app callback
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.unregisterPreferredAudioProfilesChangedCallback(
+ mPreferredAudioProfilesChangedCallback, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * A callback for preferred audio profile changes that arise from calls to
+ * {@link #setPreferredAudioProfiles(BluetoothDevice, Bundle)}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface PreferredAudioProfilesChangedCallback {
+ /**
+ * Called when the preferred audio profile change from a call to
+ * {@link #setPreferredAudioProfiles(BluetoothDevice, Bundle)} has taken effect in the audio
+ * framework or timed out. This callback includes a Bundle that indicates the current
+ * preferred audio profile for each audio mode, if one was set. If an audio mode does not
+ * have a profile preference, its key will be omitted from the Bundle. If both audio modes
+ * do not have a preferred profile set, the Bundle will be empty.
+ *
+ * <p>
+ * The Bundle can contain the following mappings:
+ * <ul>
+ * <li>For key {@link #AUDIO_MODE_OUTPUT_ONLY}, if an audio profile preference was set, this
+ * will have an int value of either {@link BluetoothProfile#A2DP} or
+ * {@link BluetoothProfile#LE_AUDIO}.
+ * <li>For key {@link #AUDIO_MODE_DUPLEX}, if an audio profile preference was set, this will
+ * have an int value of either {@link BluetoothProfile#HEADSET} or
+ * {@link BluetoothProfile#LE_AUDIO}.
+ * </ul>
+ *
+ * @param device is the device which had its preferred audio profiles changed
+ * @param preferredAudioProfiles a Bundle mapping audio mode to its preferred audio profile
+ * @param status whether the operation succeeded or timed out
+ *
+ * @hide
+ */
+ @SystemApi
+ void onPreferredAudioProfilesChanged(@NonNull BluetoothDevice device, @NonNull
+ Bundle preferredAudioProfiles, int status);
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothQualityReportReadyCallback mBluetoothQualityReportReadyCallback =
+ new IBluetoothQualityReportReadyCallback.Stub() {
+ @Override
+ public void onBluetoothQualityReportReady(BluetoothDevice device,
+ BluetoothQualityReport bluetoothQualityReport, int status) {
+ for (Map.Entry<BluetoothQualityReportReadyCallback, Executor>
+ callbackExecutorEntry:
+ mBluetoothQualityReportReadyCallbackExecutorMap.entrySet()) {
+ BluetoothQualityReportReadyCallback callback =
+ callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onBluetoothQualityReportReady(device,
+ bluetoothQualityReport, status));
+ }
+ }
+ };
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_UNKNOWN
+ })
+ public @interface RegisterBluetoothQualityReportReadyCallbackReturnValues {}
+
+ /**
+ * Registers a callback to be notified when Bluetooth Quality Report is ready. To unregister
+ * this callback, call
+ * {@link #unregisterBluetoothQualityReportReadyCallback(
+ * BluetoothQualityReportReadyCallback)}.
+ *
+ * @param executor an {@link Executor} to execute the callbacks
+ * @param callback user implementation of the {@link BluetoothQualityReportReadyCallback}
+ * @return whether the callback was registered successfully
+ * @throws NullPointerException if executor or callback is null
+ * @throws IllegalArgumentException if the callback is already registered
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @RegisterBluetoothQualityReportReadyCallbackReturnValues
+ public int registerBluetoothQualityReportReadyCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BluetoothQualityReportReadyCallback callback) {
+ if (DBG) Log.d(TAG, "registerBluetoothQualityReportReadyCallback()");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+
+ final int defaultValue = BluetoothStatusCodes.ERROR_UNKNOWN;
+ int serviceCallStatus = defaultValue;
+ synchronized (mBluetoothQualityReportReadyCallbackExecutorMap) {
+ // If the callback map is empty, we register the service-to-app callback
+ if (mBluetoothQualityReportReadyCallbackExecutorMap.isEmpty()) {
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ mService.registerBluetoothQualityReportReadyCallback(
+ mBluetoothQualityReportReadyCallback, mAttributionSource, recv);
+ serviceCallStatus = recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(defaultValue);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ if (serviceCallStatus != BluetoothStatusCodes.SUCCESS) {
+ return serviceCallStatus;
+ }
+ }
+
+ // Adds the passed in callback to our local mapping
+ if (mBluetoothQualityReportReadyCallbackExecutorMap.containsKey(callback)) {
+ throw new IllegalArgumentException("This callback has already been registered");
+ } else {
+ mBluetoothQualityReportReadyCallbackExecutorMap.put(callback, executor);
+ }
+ }
+
+ return BluetoothStatusCodes.SUCCESS;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_CALLBACK_NOT_REGISTERED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_UNKNOWN
+ })
+ public @interface UnRegisterBluetoothQualityReportReadyCallbackReturnValues {}
+
+ /**
+ * Unregisters a callback that was previously registered with
+ * {@link #registerBluetoothQualityReportReadyCallback(Executor,
+ * BluetoothQualityReportReadyCallback)}.
+ *
+ * @param callback user implementation of the {@link BluetoothQualityReportReadyCallback}
+ * @return whether the callback was successfully unregistered
+ * @throws NullPointerException if the callback is null
+ * @throws IllegalArgumentException if the callback has not been registered
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @UnRegisterBluetoothQualityReportReadyCallbackReturnValues
+ public int unregisterBluetoothQualityReportReadyCallback(
+ @NonNull BluetoothQualityReportReadyCallback callback) {
+ if (DBG) Log.d(TAG, "unregisterBluetoothQualityReportReadyCallback()");
+ requireNonNull(callback, "callback cannot be null");
+
+ synchronized (mBluetoothQualityReportReadyCallbackExecutorMap) {
+ if (mBluetoothQualityReportReadyCallbackExecutorMap.remove(callback) == null) {
+ throw new IllegalArgumentException("This callback has not been registered");
+ }
+ }
+
+ if (!mBluetoothConnectionCallbackExecutorMap.isEmpty()) {
+ return BluetoothStatusCodes.SUCCESS;
+ }
+
+ final int defaultValue = BluetoothStatusCodes.ERROR_UNKNOWN;
+ // If the callback map is empty, we unregister the service-to-app callback
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.unregisterBluetoothQualityReportReadyCallback(
+ mBluetoothQualityReportReadyCallback, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * A callback for Bluetooth Quality Report that arise from the controller.
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface BluetoothQualityReportReadyCallback {
+ /**
+ * Called when the Bluetooth Quality Report coming from the controller is ready. This
+ * callback includes a Parcel that contains information about Bluetooth Quality.
+ * Currently the report supports five event types: Quality monitor event,
+ * Approaching LSTO event, A2DP choppy event, SCO choppy event and Connect fail event.
+ * To know which kind of event is wrapped in this {@link BluetoothQualityReport} object,
+ * you need to call {@link #getQualityReportId}.
+ *
+ *
+ * @param device is the BluetoothDevice which connection quality is being reported
+ * @param bluetoothQualityReport a Parcel that contains info about Bluetooth Quality
+ * @param status whether the operation succeeded or timed out
+ *
+ * @hide
+ */
+ @SystemApi
+ void onBluetoothQualityReportReady(@NonNull BluetoothDevice device, @NonNull
+ BluetoothQualityReport bluetoothQualityReport, int status);
+ }
+
+ /**
+ * Converts old constant of priority to the new for connection policy
+ *
+ * @param priority is the priority to convert to connection policy
+ * @return the equivalent connection policy constant to the priority
+ *
+ * @hide
+ */
+ public static @ConnectionPolicy int priorityToConnectionPolicy(int priority) {
+ switch(priority) {
+ case BluetoothProfile.PRIORITY_AUTO_CONNECT:
+ return BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+ case BluetoothProfile.PRIORITY_ON:
+ return BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+ case BluetoothProfile.PRIORITY_OFF:
+ return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ case BluetoothProfile.PRIORITY_UNDEFINED:
+ return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+ default:
+ Log.e(TAG, "setPriority: Invalid priority: " + priority);
+ return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+ }
+ }
+
+ /**
+ * Converts new constant of connection policy to the old for priority
+ *
+ * @param connectionPolicy is the connection policy to convert to priority
+ * @return the equivalent priority constant to the connectionPolicy
+ *
+ * @hide
+ */
+ public static int connectionPolicyToPriority(@ConnectionPolicy int connectionPolicy) {
+ switch(connectionPolicy) {
+ case BluetoothProfile.CONNECTION_POLICY_ALLOWED:
+ return BluetoothProfile.PRIORITY_ON;
+ case BluetoothProfile.CONNECTION_POLICY_FORBIDDEN:
+ return BluetoothProfile.PRIORITY_OFF;
+ case BluetoothProfile.CONNECTION_POLICY_UNKNOWN:
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+
+ /**
+ * Sets the desired mode of the HCI snoop logging applied at Bluetooth startup.
+ *
+ * Please note that Bluetooth needs to be restarted in order for the change
+ * to take effect.
+ *
+ * @param mode
+ * @return status code indicating whether the logging mode was successfully set
+ * @throws IllegalArgumentException if the mode is not a valid logging mode
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @SetSnoopLogModeStatusCode
+ public int setBluetoothHciSnoopLoggingMode(@BluetoothSnoopLogMode int mode) {
+ if (mode != BT_SNOOP_LOG_MODE_DISABLED && mode != BT_SNOOP_LOG_MODE_FILTERED
+ && mode != BT_SNOOP_LOG_MODE_FULL) {
+ throw new IllegalArgumentException("Invalid Bluetooth HCI snoop log mode param value");
+ }
+ try {
+ return mManagerService.setBtHciSnoopLogMode(mode);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Gets the current desired mode of HCI snoop logging applied at Bluetooth startup.
+ *
+ * @return the current HCI snoop logging mode applied at Bluetooth startup
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @BluetoothSnoopLogMode
+ public int getBluetoothHciSnoopLoggingMode() {
+ try {
+ return mManagerService.getBtHciSnoopLogMode();
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return BT_SNOOP_LOG_MODE_DISABLED;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.FEATURE_SUPPORTED,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_SCAN_PERMISSION,
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED
+ })
+ public @interface GetOffloadedTransportDiscoveryDataScanSupportedReturnValues {}
+
+ /**
+ * Check if offloaded transport discovery data scan is supported or not.
+ *
+ * @return {@code BluetoothStatusCodes.FEATURE_SUPPORTED} if chipset supports on-chip tds
+ * filter scan
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @GetOffloadedTransportDiscoveryDataScanSupportedReturnValues
+ public int getOffloadedTransportDiscoveryDataScanSupported() {
+ if (!getLeAccess()) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ mServiceLock.readLock().lock();
+ try {
+ if (mService != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.getOffloadedTransportDiscoveryDataScanSupported(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_UNKNOWN);
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothAssignedNumbers.java b/android-34/android/bluetooth/BluetoothAssignedNumbers.java
new file mode 100644
index 0000000..2e83485
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothAssignedNumbers.java
@@ -0,0 +1,1200 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.SystemApi;
+
+/**
+ * Bluetooth Assigned Numbers.
+ * <p>
+ * For now we only include Company ID values.
+ *
+ * @see <a href="https://www.bluetooth.org/technical/assignednumbers/identifiers.htm"> The Official
+ * Bluetooth SIG Member Website | Company Identifiers</a>
+ */
+public class BluetoothAssignedNumbers {
+
+ // Bluetooth SIG Company ID values
+ /*
+ * Ericsson Technology Licensing.
+ */
+ public static final int ERICSSON_TECHNOLOGY = 0x0000;
+
+ /*
+ * Nokia Mobile Phones.
+ */
+ public static final int NOKIA_MOBILE_PHONES = 0x0001;
+
+ /*
+ * Intel Corp.
+ */
+ public static final int INTEL = 0x0002;
+
+ /*
+ * IBM Corp.
+ */
+ public static final int IBM = 0x0003;
+
+ /*
+ * Toshiba Corp.
+ */
+ public static final int TOSHIBA = 0x0004;
+
+ /*
+ * 3Com.
+ */
+ public static final int THREECOM = 0x0005;
+
+ /*
+ * Microsoft.
+ */
+ public static final int MICROSOFT = 0x0006;
+
+ /*
+ * Lucent.
+ */
+ public static final int LUCENT = 0x0007;
+
+ /*
+ * Motorola.
+ */
+ public static final int MOTOROLA = 0x0008;
+
+ /*
+ * Infineon Technologies AG.
+ */
+ public static final int INFINEON_TECHNOLOGIES = 0x0009;
+
+ /*
+ * Cambridge Silicon Radio.
+ */
+ public static final int CAMBRIDGE_SILICON_RADIO = 0x000A;
+
+ /*
+ * Silicon Wave.
+ */
+ public static final int SILICON_WAVE = 0x000B;
+
+ /*
+ * Digianswer A/S.
+ */
+ public static final int DIGIANSWER = 0x000C;
+
+ /*
+ * Texas Instruments Inc.
+ */
+ public static final int TEXAS_INSTRUMENTS = 0x000D;
+
+ /*
+ * Parthus Technologies Inc.
+ */
+ public static final int PARTHUS_TECHNOLOGIES = 0x000E;
+
+ /*
+ * Broadcom Corporation.
+ */
+ public static final int BROADCOM = 0x000F;
+
+ /*
+ * Mitel Semiconductor.
+ */
+ public static final int MITEL_SEMICONDUCTOR = 0x0010;
+
+ /*
+ * Widcomm, Inc.
+ */
+ public static final int WIDCOMM = 0x0011;
+
+ /*
+ * Zeevo, Inc.
+ */
+ public static final int ZEEVO = 0x0012;
+
+ /*
+ * Atmel Corporation.
+ */
+ public static final int ATMEL = 0x0013;
+
+ /*
+ * Mitsubishi Electric Corporation.
+ */
+ public static final int MITSUBISHI_ELECTRIC = 0x0014;
+
+ /*
+ * RTX Telecom A/S.
+ */
+ public static final int RTX_TELECOM = 0x0015;
+
+ /*
+ * KC Technology Inc.
+ */
+ public static final int KC_TECHNOLOGY = 0x0016;
+
+ /*
+ * Newlogic.
+ */
+ public static final int NEWLOGIC = 0x0017;
+
+ /*
+ * Transilica, Inc.
+ */
+ public static final int TRANSILICA = 0x0018;
+
+ /*
+ * Rohde & Schwarz GmbH & Co. KG.
+ */
+ public static final int ROHDE_AND_SCHWARZ = 0x0019;
+
+ /*
+ * TTPCom Limited.
+ */
+ public static final int TTPCOM = 0x001A;
+
+ /*
+ * Signia Technologies, Inc.
+ */
+ public static final int SIGNIA_TECHNOLOGIES = 0x001B;
+
+ /*
+ * Conexant Systems Inc.
+ */
+ public static final int CONEXANT_SYSTEMS = 0x001C;
+
+ /*
+ * Qualcomm.
+ */
+ public static final int QUALCOMM = 0x001D;
+
+ /*
+ * Inventel.
+ */
+ public static final int INVENTEL = 0x001E;
+
+ /*
+ * AVM Berlin.
+ */
+ public static final int AVM_BERLIN = 0x001F;
+
+ /*
+ * BandSpeed, Inc.
+ */
+ public static final int BANDSPEED = 0x0020;
+
+ /*
+ * Mansella Ltd.
+ */
+ public static final int MANSELLA = 0x0021;
+
+ /*
+ * NEC Corporation.
+ */
+ public static final int NEC = 0x0022;
+
+ /*
+ * WavePlus Technology Co., Ltd.
+ */
+ public static final int WAVEPLUS_TECHNOLOGY = 0x0023;
+
+ /*
+ * Alcatel.
+ */
+ public static final int ALCATEL = 0x0024;
+
+ /*
+ * Philips Semiconductors.
+ */
+ public static final int PHILIPS_SEMICONDUCTORS = 0x0025;
+
+ /*
+ * C Technologies.
+ */
+ public static final int C_TECHNOLOGIES = 0x0026;
+
+ /*
+ * Open Interface.
+ */
+ public static final int OPEN_INTERFACE = 0x0027;
+
+ /*
+ * R F Micro Devices.
+ */
+ public static final int RF_MICRO_DEVICES = 0x0028;
+
+ /*
+ * Hitachi Ltd.
+ */
+ public static final int HITACHI = 0x0029;
+
+ /*
+ * Symbol Technologies, Inc.
+ */
+ public static final int SYMBOL_TECHNOLOGIES = 0x002A;
+
+ /*
+ * Tenovis.
+ */
+ public static final int TENOVIS = 0x002B;
+
+ /*
+ * Macronix International Co. Ltd.
+ */
+ public static final int MACRONIX = 0x002C;
+
+ /*
+ * GCT Semiconductor.
+ */
+ public static final int GCT_SEMICONDUCTOR = 0x002D;
+
+ /*
+ * Norwood Systems.
+ */
+ public static final int NORWOOD_SYSTEMS = 0x002E;
+
+ /*
+ * MewTel Technology Inc.
+ */
+ public static final int MEWTEL_TECHNOLOGY = 0x002F;
+
+ /*
+ * ST Microelectronics.
+ */
+ public static final int ST_MICROELECTRONICS = 0x0030;
+
+ /*
+ * Synopsys.
+ */
+ public static final int SYNOPSYS = 0x0031;
+
+ /*
+ * Red-M (Communications) Ltd.
+ */
+ public static final int RED_M = 0x0032;
+
+ /*
+ * Commil Ltd.
+ */
+ public static final int COMMIL = 0x0033;
+
+ /*
+ * Computer Access Technology Corporation (CATC).
+ */
+ public static final int CATC = 0x0034;
+
+ /*
+ * Eclipse (HQ Espana) S.L.
+ */
+ public static final int ECLIPSE = 0x0035;
+
+ /*
+ * Renesas Technology Corp.
+ */
+ public static final int RENESAS_TECHNOLOGY = 0x0036;
+
+ /*
+ * Mobilian Corporation.
+ */
+ public static final int MOBILIAN_CORPORATION = 0x0037;
+
+ /*
+ * Terax.
+ */
+ public static final int TERAX = 0x0038;
+
+ /*
+ * Integrated System Solution Corp.
+ */
+ public static final int INTEGRATED_SYSTEM_SOLUTION = 0x0039;
+
+ /*
+ * Matsushita Electric Industrial Co., Ltd.
+ */
+ public static final int MATSUSHITA_ELECTRIC = 0x003A;
+
+ /*
+ * Gennum Corporation.
+ */
+ public static final int GENNUM = 0x003B;
+
+ /*
+ * Research In Motion.
+ */
+ public static final int RESEARCH_IN_MOTION = 0x003C;
+
+ /*
+ * IPextreme, Inc.
+ */
+ public static final int IPEXTREME = 0x003D;
+
+ /*
+ * Systems and Chips, Inc.
+ */
+ public static final int SYSTEMS_AND_CHIPS = 0x003E;
+
+ /*
+ * Bluetooth SIG, Inc.
+ */
+ public static final int BLUETOOTH_SIG = 0x003F;
+
+ /*
+ * Seiko Epson Corporation.
+ */
+ public static final int SEIKO_EPSON = 0x0040;
+
+ /*
+ * Integrated Silicon Solution Taiwan, Inc.
+ */
+ public static final int INTEGRATED_SILICON_SOLUTION = 0x0041;
+
+ /*
+ * CONWISE Technology Corporation Ltd.
+ */
+ public static final int CONWISE_TECHNOLOGY = 0x0042;
+
+ /*
+ * PARROT SA.
+ */
+ public static final int PARROT = 0x0043;
+
+ /*
+ * Socket Mobile.
+ */
+ public static final int SOCKET_MOBILE = 0x0044;
+
+ /*
+ * Atheros Communications, Inc.
+ */
+ public static final int ATHEROS_COMMUNICATIONS = 0x0045;
+
+ /*
+ * MediaTek, Inc.
+ */
+ public static final int MEDIATEK = 0x0046;
+
+ /*
+ * Bluegiga.
+ */
+ public static final int BLUEGIGA = 0x0047;
+
+ /*
+ * Marvell Technology Group Ltd.
+ */
+ public static final int MARVELL = 0x0048;
+
+ /*
+ * 3DSP Corporation.
+ */
+ public static final int THREE_DSP = 0x0049;
+
+ /*
+ * Accel Semiconductor Ltd.
+ */
+ public static final int ACCEL_SEMICONDUCTOR = 0x004A;
+
+ /*
+ * Continental Automotive Systems.
+ */
+ public static final int CONTINENTAL_AUTOMOTIVE = 0x004B;
+
+ /*
+ * Apple, Inc.
+ */
+ public static final int APPLE = 0x004C;
+
+ /*
+ * Staccato Communications, Inc.
+ */
+ public static final int STACCATO_COMMUNICATIONS = 0x004D;
+
+ /*
+ * Avago Technologies.
+ */
+ public static final int AVAGO = 0x004E;
+
+ /*
+ * APT Licensing Ltd.
+ */
+ public static final int APT_LICENSING = 0x004F;
+
+ /*
+ * SiRF Technology, Inc.
+ */
+ public static final int SIRF_TECHNOLOGY = 0x0050;
+
+ /*
+ * Tzero Technologies, Inc.
+ */
+ public static final int TZERO_TECHNOLOGIES = 0x0051;
+
+ /*
+ * J&M Corporation.
+ */
+ public static final int J_AND_M = 0x0052;
+
+ /*
+ * Free2move AB.
+ */
+ public static final int FREE2MOVE = 0x0053;
+
+ /*
+ * 3DiJoy Corporation.
+ */
+ public static final int THREE_DIJOY = 0x0054;
+
+ /*
+ * Plantronics, Inc.
+ */
+ public static final int PLANTRONICS = 0x0055;
+
+ /*
+ * Sony Ericsson Mobile Communications.
+ */
+ public static final int SONY_ERICSSON = 0x0056;
+
+ /*
+ * Harman International Industries, Inc.
+ */
+ public static final int HARMAN_INTERNATIONAL = 0x0057;
+
+ /*
+ * Vizio, Inc.
+ */
+ public static final int VIZIO = 0x0058;
+
+ /*
+ * Nordic Semiconductor ASA.
+ */
+ public static final int NORDIC_SEMICONDUCTOR = 0x0059;
+
+ /*
+ * EM Microelectronic-Marin SA.
+ */
+ public static final int EM_MICROELECTRONIC_MARIN = 0x005A;
+
+ /*
+ * Ralink Technology Corporation.
+ */
+ public static final int RALINK_TECHNOLOGY = 0x005B;
+
+ /*
+ * Belkin International, Inc.
+ */
+ public static final int BELKIN_INTERNATIONAL = 0x005C;
+
+ /*
+ * Realtek Semiconductor Corporation.
+ */
+ public static final int REALTEK_SEMICONDUCTOR = 0x005D;
+
+ /*
+ * Stonestreet One, LLC.
+ */
+ public static final int STONESTREET_ONE = 0x005E;
+
+ /*
+ * Wicentric, Inc.
+ */
+ public static final int WICENTRIC = 0x005F;
+
+ /*
+ * RivieraWaves S.A.S.
+ */
+ public static final int RIVIERAWAVES = 0x0060;
+
+ /*
+ * RDA Microelectronics.
+ */
+ public static final int RDA_MICROELECTRONICS = 0x0061;
+
+ /*
+ * Gibson Guitars.
+ */
+ public static final int GIBSON_GUITARS = 0x0062;
+
+ /*
+ * MiCommand Inc.
+ */
+ public static final int MICOMMAND = 0x0063;
+
+ /*
+ * Band XI International, LLC.
+ */
+ public static final int BAND_XI_INTERNATIONAL = 0x0064;
+
+ /*
+ * Hewlett-Packard Company.
+ */
+ public static final int HEWLETT_PACKARD = 0x0065;
+
+ /*
+ * 9Solutions Oy.
+ */
+ public static final int NINE_SOLUTIONS = 0x0066;
+
+ /*
+ * GN Netcom A/S.
+ */
+ public static final int GN_NETCOM = 0x0067;
+
+ /*
+ * General Motors.
+ */
+ public static final int GENERAL_MOTORS = 0x0068;
+
+ /*
+ * A&D Engineering, Inc.
+ */
+ public static final int A_AND_D_ENGINEERING = 0x0069;
+
+ /*
+ * MindTree Ltd.
+ */
+ public static final int MINDTREE = 0x006A;
+
+ /*
+ * Polar Electro OY.
+ */
+ public static final int POLAR_ELECTRO = 0x006B;
+
+ /*
+ * Beautiful Enterprise Co., Ltd.
+ */
+ public static final int BEAUTIFUL_ENTERPRISE = 0x006C;
+
+ /*
+ * BriarTek, Inc.
+ */
+ public static final int BRIARTEK = 0x006D;
+
+ /*
+ * Summit Data Communications, Inc.
+ */
+ public static final int SUMMIT_DATA_COMMUNICATIONS = 0x006E;
+
+ /*
+ * Sound ID.
+ */
+ public static final int SOUND_ID = 0x006F;
+
+ /*
+ * Monster, LLC.
+ */
+ public static final int MONSTER = 0x0070;
+
+ /*
+ * connectBlue AB.
+ */
+ public static final int CONNECTBLUE = 0x0071;
+
+ /*
+ * ShangHai Super Smart Electronics Co. Ltd.
+ */
+ public static final int SHANGHAI_SUPER_SMART_ELECTRONICS = 0x0072;
+
+ /*
+ * Group Sense Ltd.
+ */
+ public static final int GROUP_SENSE = 0x0073;
+
+ /*
+ * Zomm, LLC.
+ */
+ public static final int ZOMM = 0x0074;
+
+ /*
+ * Samsung Electronics Co. Ltd.
+ */
+ public static final int SAMSUNG_ELECTRONICS = 0x0075;
+
+ /*
+ * Creative Technology Ltd.
+ */
+ public static final int CREATIVE_TECHNOLOGY = 0x0076;
+
+ /*
+ * Laird Technologies.
+ */
+ public static final int LAIRD_TECHNOLOGIES = 0x0077;
+
+ /*
+ * Nike, Inc.
+ */
+ public static final int NIKE = 0x0078;
+
+ /*
+ * lesswire AG.
+ */
+ public static final int LESSWIRE = 0x0079;
+
+ /*
+ * MStar Semiconductor, Inc.
+ */
+ public static final int MSTAR_SEMICONDUCTOR = 0x007A;
+
+ /*
+ * Hanlynn Technologies.
+ */
+ public static final int HANLYNN_TECHNOLOGIES = 0x007B;
+
+ /*
+ * A & R Cambridge.
+ */
+ public static final int A_AND_R_CAMBRIDGE = 0x007C;
+
+ /*
+ * Seers Technology Co. Ltd.
+ */
+ public static final int SEERS_TECHNOLOGY = 0x007D;
+
+ /*
+ * Sports Tracking Technologies Ltd.
+ */
+ public static final int SPORTS_TRACKING_TECHNOLOGIES = 0x007E;
+
+ /*
+ * Autonet Mobile.
+ */
+ public static final int AUTONET_MOBILE = 0x007F;
+
+ /*
+ * DeLorme Publishing Company, Inc.
+ */
+ public static final int DELORME_PUBLISHING_COMPANY = 0x0080;
+
+ /*
+ * WuXi Vimicro.
+ */
+ public static final int WUXI_VIMICRO = 0x0081;
+
+ /*
+ * Sennheiser Communications A/S.
+ */
+ public static final int SENNHEISER_COMMUNICATIONS = 0x0082;
+
+ /*
+ * TimeKeeping Systems, Inc.
+ */
+ public static final int TIMEKEEPING_SYSTEMS = 0x0083;
+
+ /*
+ * Ludus Helsinki Ltd.
+ */
+ public static final int LUDUS_HELSINKI = 0x0084;
+
+ /*
+ * BlueRadios, Inc.
+ */
+ public static final int BLUERADIOS = 0x0085;
+
+ /*
+ * equinox AG.
+ */
+ public static final int EQUINOX_AG = 0x0086;
+
+ /*
+ * Garmin International, Inc.
+ */
+ public static final int GARMIN_INTERNATIONAL = 0x0087;
+
+ /*
+ * Ecotest.
+ */
+ public static final int ECOTEST = 0x0088;
+
+ /*
+ * GN ReSound A/S.
+ */
+ public static final int GN_RESOUND = 0x0089;
+
+ /*
+ * Jawbone.
+ */
+ public static final int JAWBONE = 0x008A;
+
+ /*
+ * Topcorn Positioning Systems, LLC.
+ */
+ public static final int TOPCORN_POSITIONING_SYSTEMS = 0x008B;
+
+ /*
+ * Qualcomm Labs, Inc.
+ */
+ public static final int QUALCOMM_LABS = 0x008C;
+
+ /*
+ * Zscan Software.
+ */
+ public static final int ZSCAN_SOFTWARE = 0x008D;
+
+ /*
+ * Quintic Corp.
+ */
+ public static final int QUINTIC = 0x008E;
+
+ /*
+ * Stollman E+V GmbH.
+ */
+ public static final int STOLLMAN_E_PLUS_V = 0x008F;
+
+ /*
+ * Funai Electric Co., Ltd.
+ */
+ public static final int FUNAI_ELECTRIC = 0x0090;
+
+ /*
+ * Advanced PANMOBIL Systems GmbH & Co. KG.
+ */
+ public static final int ADVANCED_PANMOBIL_SYSTEMS = 0x0091;
+
+ /*
+ * ThinkOptics, Inc.
+ */
+ public static final int THINKOPTICS = 0x0092;
+
+ /*
+ * Universal Electronics, Inc.
+ */
+ public static final int UNIVERSAL_ELECTRONICS = 0x0093;
+
+ /*
+ * Airoha Technology Corp.
+ */
+ public static final int AIROHA_TECHNOLOGY = 0x0094;
+
+ /*
+ * NEC Lighting, Ltd.
+ */
+ public static final int NEC_LIGHTING = 0x0095;
+
+ /*
+ * ODM Technology, Inc.
+ */
+ public static final int ODM_TECHNOLOGY = 0x0096;
+
+ /*
+ * Bluetrek Technologies Limited.
+ */
+ public static final int BLUETREK_TECHNOLOGIES = 0x0097;
+
+ /*
+ * zer01.tv GmbH.
+ */
+ public static final int ZER01_TV = 0x0098;
+
+ /*
+ * i.Tech Dynamic Global Distribution Ltd.
+ */
+ public static final int I_TECH_DYNAMIC_GLOBAL_DISTRIBUTION = 0x0099;
+
+ /*
+ * Alpwise.
+ */
+ public static final int ALPWISE = 0x009A;
+
+ /*
+ * Jiangsu Toppower Automotive Electronics Co., Ltd.
+ */
+ public static final int JIANGSU_TOPPOWER_AUTOMOTIVE_ELECTRONICS = 0x009B;
+
+ /*
+ * Colorfy, Inc.
+ */
+ public static final int COLORFY = 0x009C;
+
+ /*
+ * Geoforce Inc.
+ */
+ public static final int GEOFORCE = 0x009D;
+
+ /*
+ * Bose Corporation.
+ */
+ public static final int BOSE = 0x009E;
+
+ /*
+ * Suunto Oy.
+ */
+ public static final int SUUNTO = 0x009F;
+
+ /*
+ * Kensington Computer Products Group.
+ */
+ public static final int KENSINGTON_COMPUTER_PRODUCTS_GROUP = 0x00A0;
+
+ /*
+ * SR-Medizinelektronik.
+ */
+ public static final int SR_MEDIZINELEKTRONIK = 0x00A1;
+
+ /*
+ * Vertu Corporation Limited.
+ */
+ public static final int VERTU = 0x00A2;
+
+ /*
+ * Meta Watch Ltd.
+ */
+ public static final int META_WATCH = 0x00A3;
+
+ /*
+ * LINAK A/S.
+ */
+ public static final int LINAK = 0x00A4;
+
+ /*
+ * OTL Dynamics LLC.
+ */
+ public static final int OTL_DYNAMICS = 0x00A5;
+
+ /*
+ * Panda Ocean Inc.
+ */
+ public static final int PANDA_OCEAN = 0x00A6;
+
+ /*
+ * Visteon Corporation.
+ */
+ public static final int VISTEON = 0x00A7;
+
+ /*
+ * ARP Devices Limited.
+ */
+ public static final int ARP_DEVICES = 0x00A8;
+
+ /*
+ * Magneti Marelli S.p.A.
+ */
+ public static final int MAGNETI_MARELLI = 0x00A9;
+
+ /*
+ * CAEN RFID srl.
+ */
+ public static final int CAEN_RFID = 0x00AA;
+
+ /*
+ * Ingenieur-Systemgruppe Zahn GmbH.
+ */
+ public static final int INGENIEUR_SYSTEMGRUPPE_ZAHN = 0x00AB;
+
+ /*
+ * Green Throttle Games.
+ */
+ public static final int GREEN_THROTTLE_GAMES = 0x00AC;
+
+ /*
+ * Peter Systemtechnik GmbH.
+ */
+ public static final int PETER_SYSTEMTECHNIK = 0x00AD;
+
+ /*
+ * Omegawave Oy.
+ */
+ public static final int OMEGAWAVE = 0x00AE;
+
+ /*
+ * Cinetix.
+ */
+ public static final int CINETIX = 0x00AF;
+
+ /*
+ * Passif Semiconductor Corp.
+ */
+ public static final int PASSIF_SEMICONDUCTOR = 0x00B0;
+
+ /*
+ * Saris Cycling Group, Inc.
+ */
+ public static final int SARIS_CYCLING_GROUP = 0x00B1;
+
+ /*
+ * Bekey A/S.
+ */
+ public static final int BEKEY = 0x00B2;
+
+ /*
+ * Clarinox Technologies Pty. Ltd.
+ */
+ public static final int CLARINOX_TECHNOLOGIES = 0x00B3;
+
+ /*
+ * BDE Technology Co., Ltd.
+ */
+ public static final int BDE_TECHNOLOGY = 0x00B4;
+
+ /*
+ * Swirl Networks.
+ */
+ public static final int SWIRL_NETWORKS = 0x00B5;
+
+ /*
+ * Meso international.
+ */
+ public static final int MESO_INTERNATIONAL = 0x00B6;
+
+ /*
+ * TreLab Ltd.
+ */
+ public static final int TRELAB = 0x00B7;
+
+ /*
+ * Qualcomm Innovation Center, Inc. (QuIC).
+ */
+ public static final int QUALCOMM_INNOVATION_CENTER = 0x00B8;
+
+ /*
+ * Johnson Controls, Inc.
+ */
+ public static final int JOHNSON_CONTROLS = 0x00B9;
+
+ /*
+ * Starkey Laboratories Inc.
+ */
+ public static final int STARKEY_LABORATORIES = 0x00BA;
+
+ /*
+ * S-Power Electronics Limited.
+ */
+ public static final int S_POWER_ELECTRONICS = 0x00BB;
+
+ /*
+ * Ace Sensor Inc.
+ */
+ public static final int ACE_SENSOR = 0x00BC;
+
+ /*
+ * Aplix Corporation.
+ */
+ public static final int APLIX = 0x00BD;
+
+ /*
+ * AAMP of America.
+ */
+ public static final int AAMP_OF_AMERICA = 0x00BE;
+
+ /*
+ * Stalmart Technology Limited.
+ */
+ public static final int STALMART_TECHNOLOGY = 0x00BF;
+
+ /*
+ * AMICCOM Electronics Corporation.
+ */
+ public static final int AMICCOM_ELECTRONICS = 0x00C0;
+
+ /*
+ * Shenzhen Excelsecu Data Technology Co.,Ltd.
+ */
+ public static final int SHENZHEN_EXCELSECU_DATA_TECHNOLOGY = 0x00C1;
+
+ /*
+ * Geneq Inc.
+ */
+ public static final int GENEQ = 0x00C2;
+
+ /*
+ * adidas AG.
+ */
+ public static final int ADIDAS = 0x00C3;
+
+ /*
+ * LG Electronics.
+ */
+ public static final int LG_ELECTRONICS = 0x00C4;
+
+ /*
+ * Onset Computer Corporation.
+ */
+ public static final int ONSET_COMPUTER = 0x00C5;
+
+ /*
+ * Selfly BV.
+ */
+ public static final int SELFLY = 0x00C6;
+
+ /*
+ * Quuppa Oy.
+ */
+ public static final int QUUPPA = 0x00C7;
+
+ /*
+ * GeLo Inc.
+ */
+ public static final int GELO = 0x00C8;
+
+ /*
+ * Evluma.
+ */
+ public static final int EVLUMA = 0x00C9;
+
+ /*
+ * MC10.
+ */
+ public static final int MC10 = 0x00CA;
+
+ /*
+ * Binauric SE.
+ */
+ public static final int BINAURIC = 0x00CB;
+
+ /*
+ * Beats Electronics.
+ */
+ public static final int BEATS_ELECTRONICS = 0x00CC;
+
+ /*
+ * Microchip Technology Inc.
+ */
+ public static final int MICROCHIP_TECHNOLOGY = 0x00CD;
+
+ /*
+ * Elgato Systems GmbH.
+ */
+ public static final int ELGATO_SYSTEMS = 0x00CE;
+
+ /*
+ * ARCHOS SA.
+ */
+ public static final int ARCHOS = 0x00CF;
+
+ /*
+ * Dexcom, Inc.
+ */
+ public static final int DEXCOM = 0x00D0;
+
+ /*
+ * Polar Electro Europe B.V.
+ */
+ public static final int POLAR_ELECTRO_EUROPE = 0x00D1;
+
+ /*
+ * Dialog Semiconductor B.V.
+ */
+ public static final int DIALOG_SEMICONDUCTOR = 0x00D2;
+
+ /*
+ * Taixingbang Technology (HK) Co,. LTD.
+ */
+ public static final int TAIXINGBANG_TECHNOLOGY = 0x00D3;
+
+ /*
+ * Kawantech.
+ */
+ public static final int KAWANTECH = 0x00D4;
+
+ /*
+ * Austco Communication Systems.
+ */
+ public static final int AUSTCO_COMMUNICATION_SYSTEMS = 0x00D5;
+
+ /*
+ * Timex Group USA, Inc.
+ */
+ public static final int TIMEX_GROUP_USA = 0x00D6;
+
+ /*
+ * Qualcomm Technologies, Inc.
+ */
+ public static final int QUALCOMM_TECHNOLOGIES = 0x00D7;
+
+ /*
+ * Qualcomm Connected Experiences, Inc.
+ */
+ public static final int QUALCOMM_CONNECTED_EXPERIENCES = 0x00D8;
+
+ /*
+ * Voyetra Turtle Beach.
+ */
+ public static final int VOYETRA_TURTLE_BEACH = 0x00D9;
+
+ /*
+ * txtr GmbH.
+ */
+ public static final int TXTR = 0x00DA;
+
+ /*
+ * Biosentronics.
+ */
+ public static final int BIOSENTRONICS = 0x00DB;
+
+ /*
+ * Procter & Gamble.
+ */
+ public static final int PROCTER_AND_GAMBLE = 0x00DC;
+
+ /*
+ * Hosiden Corporation.
+ */
+ public static final int HOSIDEN = 0x00DD;
+
+ /*
+ * Muzik LLC.
+ */
+ public static final int MUZIK = 0x00DE;
+
+ /*
+ * Misfit Wearables Corp.
+ */
+ public static final int MISFIT_WEARABLES = 0x00DF;
+
+ /*
+ * Google.
+ */
+ public static final int GOOGLE = 0x00E0;
+
+ /*
+ * Danlers Ltd.
+ */
+ public static final int DANLERS = 0x00E1;
+
+ /*
+ * Semilink Inc.
+ */
+ public static final int SEMILINK = 0x00E2;
+
+ /*
+ * You can't instantiate one of these.
+ */
+ private BluetoothAssignedNumbers() {
+ }
+
+ /**
+ * The values of {@code OrganizationId} are assigned by Bluetooth SIG. For more
+ * details refer to Transport Discovery Service Organization IDs.
+ * (https://www.bluetooth.com/specifications/assigned-numbers/)
+ *
+ * @hide
+ */
+ @SystemApi
+ public class OrganizationId {
+ /*
+ * This is for Bluetooth SIG Organization ID .
+ */
+ public static final int BLUETOOTH_SIG = 0x01;
+
+ /*
+ * This is for Wi-Fi Alliance Neighbor Awareness Networking Organization ID.
+ */
+ public static final int WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING = 0x02;
+
+ /**
+ * This is for WiFi Alliance Service Advertisement Organization ID.
+ */
+ public static final int WIFI_ALLIANCE_SERVICE_ADVERTISEMENT = 0x03;
+
+ private OrganizationId() {
+ }
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothAudioConfig.java b/android-34/android/bluetooth/BluetoothAudioConfig.java
new file mode 100644
index 0000000..cd7b22a
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothAudioConfig.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents the audio configuration for a Bluetooth A2DP source device.
+ *
+ * {@see BluetoothA2dpSink}
+ *
+ * {@hide}
+ */
+public final class BluetoothAudioConfig implements Parcelable {
+
+ private final int mSampleRate;
+ private final int mChannelConfig;
+ private final int mAudioFormat;
+
+ public BluetoothAudioConfig(int sampleRate, int channelConfig, int audioFormat) {
+ mSampleRate = sampleRate;
+ mChannelConfig = channelConfig;
+ mAudioFormat = audioFormat;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothAudioConfig) {
+ BluetoothAudioConfig bac = (BluetoothAudioConfig) o;
+ return (bac.mSampleRate == mSampleRate && bac.mChannelConfig == mChannelConfig
+ && bac.mAudioFormat == mAudioFormat);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mSampleRate | (mChannelConfig << 24) | (mAudioFormat << 28);
+ }
+
+ @Override
+ public String toString() {
+ return "{mSampleRate:" + mSampleRate + ",mChannelConfig:" + mChannelConfig
+ + ",mAudioFormat:" + mAudioFormat + "}";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<BluetoothAudioConfig> CREATOR = new Creator<>() {
+ public BluetoothAudioConfig createFromParcel(Parcel in) {
+ int sampleRate = in.readInt();
+ int channelConfig = in.readInt();
+ int audioFormat = in.readInt();
+ return new BluetoothAudioConfig(sampleRate, channelConfig, audioFormat);
+ }
+
+ public BluetoothAudioConfig[] newArray(int size) {
+ return new BluetoothAudioConfig[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSampleRate);
+ out.writeInt(mChannelConfig);
+ out.writeInt(mAudioFormat);
+ }
+
+ /**
+ * Returns the sample rate in samples per second
+ *
+ * @return sample rate
+ */
+ public int getSampleRate() {
+ return mSampleRate;
+ }
+
+ /**
+ * Returns the channel configuration (either {@link android.media.AudioFormat#CHANNEL_IN_MONO}
+ * or {@link android.media.AudioFormat#CHANNEL_IN_STEREO})
+ *
+ * @return channel configuration
+ */
+ public int getChannelConfig() {
+ return mChannelConfig;
+ }
+
+ /**
+ * Returns the channel audio format (either {@link android.media.AudioFormat#ENCODING_PCM_16BIT}
+ * or {@link android.media.AudioFormat#ENCODING_PCM_8BIT}
+ *
+ * @return audio format
+ */
+ public int getAudioFormat() {
+ return mAudioFormat;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothAvrcp.java b/android-34/android/bluetooth/BluetoothAvrcp.java
new file mode 100644
index 0000000..1a4c759
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothAvrcp.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+/**
+ * This class contains constants for Bluetooth AVRCP profile.
+ *
+ * {@hide}
+ */
+public final class BluetoothAvrcp {
+
+ /*
+ * State flags for Passthrough commands
+ */
+ public static final int PASSTHROUGH_STATE_PRESS = 0;
+ public static final int PASSTHROUGH_STATE_RELEASE = 1;
+
+ /*
+ * Operation IDs for Passthrough commands
+ */
+ public static final int PASSTHROUGH_ID_SELECT = 0x00; /* select */
+ public static final int PASSTHROUGH_ID_UP = 0x01; /* up */
+ public static final int PASSTHROUGH_ID_DOWN = 0x02; /* down */
+ public static final int PASSTHROUGH_ID_LEFT = 0x03; /* left */
+ public static final int PASSTHROUGH_ID_RIGHT = 0x04; /* right */
+ public static final int PASSTHROUGH_ID_RIGHT_UP = 0x05; /* right-up */
+ public static final int PASSTHROUGH_ID_RIGHT_DOWN = 0x06; /* right-down */
+ public static final int PASSTHROUGH_ID_LEFT_UP = 0x07; /* left-up */
+ public static final int PASSTHROUGH_ID_LEFT_DOWN = 0x08; /* left-down */
+ public static final int PASSTHROUGH_ID_ROOT_MENU = 0x09; /* root menu */
+ public static final int PASSTHROUGH_ID_SETUP_MENU = 0x0A; /* setup menu */
+ public static final int PASSTHROUGH_ID_CONT_MENU = 0x0B; /* contents menu */
+ public static final int PASSTHROUGH_ID_FAV_MENU = 0x0C; /* favorite menu */
+ public static final int PASSTHROUGH_ID_EXIT = 0x0D; /* exit */
+ public static final int PASSTHROUGH_ID_0 = 0x20; /* 0 */
+ public static final int PASSTHROUGH_ID_1 = 0x21; /* 1 */
+ public static final int PASSTHROUGH_ID_2 = 0x22; /* 2 */
+ public static final int PASSTHROUGH_ID_3 = 0x23; /* 3 */
+ public static final int PASSTHROUGH_ID_4 = 0x24; /* 4 */
+ public static final int PASSTHROUGH_ID_5 = 0x25; /* 5 */
+ public static final int PASSTHROUGH_ID_6 = 0x26; /* 6 */
+ public static final int PASSTHROUGH_ID_7 = 0x27; /* 7 */
+ public static final int PASSTHROUGH_ID_8 = 0x28; /* 8 */
+ public static final int PASSTHROUGH_ID_9 = 0x29; /* 9 */
+ public static final int PASSTHROUGH_ID_DOT = 0x2A; /* dot */
+ public static final int PASSTHROUGH_ID_ENTER = 0x2B; /* enter */
+ public static final int PASSTHROUGH_ID_CLEAR = 0x2C; /* clear */
+ public static final int PASSTHROUGH_ID_CHAN_UP = 0x30; /* channel up */
+ public static final int PASSTHROUGH_ID_CHAN_DOWN = 0x31; /* channel down */
+ public static final int PASSTHROUGH_ID_PREV_CHAN = 0x32; /* previous channel */
+ public static final int PASSTHROUGH_ID_SOUND_SEL = 0x33; /* sound select */
+ public static final int PASSTHROUGH_ID_INPUT_SEL = 0x34; /* input select */
+ public static final int PASSTHROUGH_ID_DISP_INFO = 0x35; /* display information */
+ public static final int PASSTHROUGH_ID_HELP = 0x36; /* help */
+ public static final int PASSTHROUGH_ID_PAGE_UP = 0x37; /* page up */
+ public static final int PASSTHROUGH_ID_PAGE_DOWN = 0x38; /* page down */
+ public static final int PASSTHROUGH_ID_POWER = 0x40; /* power */
+ public static final int PASSTHROUGH_ID_VOL_UP = 0x41; /* volume up */
+ public static final int PASSTHROUGH_ID_VOL_DOWN = 0x42; /* volume down */
+ public static final int PASSTHROUGH_ID_MUTE = 0x43; /* mute */
+ public static final int PASSTHROUGH_ID_PLAY = 0x44; /* play */
+ public static final int PASSTHROUGH_ID_STOP = 0x45; /* stop */
+ public static final int PASSTHROUGH_ID_PAUSE = 0x46; /* pause */
+ public static final int PASSTHROUGH_ID_RECORD = 0x47; /* record */
+ public static final int PASSTHROUGH_ID_REWIND = 0x48; /* rewind */
+ public static final int PASSTHROUGH_ID_FAST_FOR = 0x49; /* fast forward */
+ public static final int PASSTHROUGH_ID_EJECT = 0x4A; /* eject */
+ public static final int PASSTHROUGH_ID_FORWARD = 0x4B; /* forward */
+ public static final int PASSTHROUGH_ID_BACKWARD = 0x4C; /* backward */
+ public static final int PASSTHROUGH_ID_ANGLE = 0x50; /* angle */
+ public static final int PASSTHROUGH_ID_SUBPICT = 0x51; /* subpicture */
+ public static final int PASSTHROUGH_ID_F1 = 0x71; /* F1 */
+ public static final int PASSTHROUGH_ID_F2 = 0x72; /* F2 */
+ public static final int PASSTHROUGH_ID_F3 = 0x73; /* F3 */
+ public static final int PASSTHROUGH_ID_F4 = 0x74; /* F4 */
+ public static final int PASSTHROUGH_ID_F5 = 0x75; /* F5 */
+ public static final int PASSTHROUGH_ID_VENDOR = 0x7E; /* vendor unique */
+ public static final int PASSTHROUGH_KEYPRESSED_RELEASE = 0x80;
+}
diff --git a/android-34/android/bluetooth/BluetoothAvrcpController.java b/android-34/android/bluetooth/BluetoothAvrcpController.java
new file mode 100644
index 0000000..e442faf
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothAvrcpController.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Bluetooth AVRCP Controller. It currently
+ * supports player information, playback support and track metadata.
+ *
+ * <p>BluetoothAvrcpController is a proxy object for controlling the Bluetooth AVRCP
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothAvrcpController proxy object.
+ *
+ * {@hide}
+ */
+public final class BluetoothAvrcpController implements BluetoothProfile {
+ private static final String TAG = "BluetoothAvrcpController";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the AVRCP Controller
+ * profile.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.avrcp-controller.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in player application setting state on AVRCP AG.
+ *
+ * <p>This intent will have the following extras:
+ * <ul>
+ * <li> {@link #EXTRA_PLAYER_SETTING} - {@link BluetoothAvrcpPlayerSettings} containing the
+ * most recent player setting. </li>
+ * </ul>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PLAYER_SETTING =
+ "android.bluetooth.avrcp-controller.profile.action.PLAYER_SETTING";
+
+ public static final String EXTRA_PLAYER_SETTING =
+ "android.bluetooth.avrcp-controller.profile.extra.PLAYER_SETTING";
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothAvrcpController> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.AVRCP_CONTROLLER,
+ "BluetoothAvrcpController", IBluetoothAvrcpController.class.getName()) {
+ @Override
+ public IBluetoothAvrcpController getServiceInterface(IBinder service) {
+ return IBluetoothAvrcpController.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothAvrcpController proxy object for interacting with the local
+ * Bluetooth AVRCP service.
+ */
+ /* package */ BluetoothAvrcpController(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothAvrcpController getService() {
+ return mProfileConnector.getService();
+ }
+
+ @Override
+ public void finalize() {
+ close();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothAvrcpController service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothAvrcpController service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothAvrcpController service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the player application settings.
+ *
+ * @return the {@link BluetoothAvrcpPlayerSettings} or {@link null} if there is an error.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothAvrcpPlayerSettings getPlayerSettings(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "getPlayerSettings");
+ BluetoothAvrcpPlayerSettings settings = null;
+ final IBluetoothAvrcpController service = getService();
+ final BluetoothAvrcpPlayerSettings defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<BluetoothAvrcpPlayerSettings> recv =
+ SynchronousResultReceiver.get();
+ service.getPlayerSettings(device, mAttributionSource, recv);
+ settings = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the player app setting for current player.
+ * returns true in case setting is supported by remote, false otherwise
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings plAppSetting) {
+ if (DBG) Log.d(TAG, "setPlayerApplicationSetting");
+ final IBluetoothAvrcpController service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setPlayerApplicationSetting(plAppSetting, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Send Group Navigation Command to Remote.
+ * possible keycode values: next_grp, previous_grp defined above
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void sendGroupNavigationCmd(BluetoothDevice device, int keyCode, int keyState) {
+ Log.d(TAG, "sendGroupNavigationCmd dev = " + device + " key " + keyCode + " State = "
+ + keyState);
+ final IBluetoothAvrcpController service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ service.sendGroupNavigationCmd(device, keyCode, keyState, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ return;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothAvrcpPlayerSettings.java b/android-34/android/bluetooth/BluetoothAvrcpPlayerSettings.java
new file mode 100644
index 0000000..4112a0d
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothAvrcpPlayerSettings.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Class used to identify settings associated with the player on AG.
+ *
+ * {@hide}
+ */
+public final class BluetoothAvrcpPlayerSettings implements Parcelable {
+ public static final String TAG = "BluetoothAvrcpPlayerSettings";
+
+ /**
+ * Equalizer setting.
+ */
+ public static final int SETTING_EQUALIZER = 0x01;
+
+ /**
+ * Repeat setting.
+ */
+ public static final int SETTING_REPEAT = 0x02;
+
+ /**
+ * Shuffle setting.
+ */
+ public static final int SETTING_SHUFFLE = 0x04;
+
+ /**
+ * Scan mode setting.
+ */
+ public static final int SETTING_SCAN = 0x08;
+
+ /**
+ * Invalid state.
+ *
+ * Used for returning error codes.
+ */
+ public static final int STATE_INVALID = -1;
+
+ /**
+ * OFF state.
+ *
+ * Denotes a general OFF state. Applies to all settings.
+ */
+ public static final int STATE_OFF = 0x00;
+
+ /**
+ * ON state.
+ *
+ * Applies to {@link SETTING_EQUALIZER}.
+ */
+ public static final int STATE_ON = 0x01;
+
+ /**
+ * Single track repeat.
+ *
+ * Applies only to {@link SETTING_REPEAT}.
+ */
+ public static final int STATE_SINGLE_TRACK = 0x02;
+
+ /**
+ * All track repeat/shuffle.
+ *
+ * Applies to {@link #SETTING_REPEAT}, {@link #SETTING_SHUFFLE} and {@link #SETTING_SCAN}.
+ */
+ public static final int STATE_ALL_TRACK = 0x03;
+
+ /**
+ * Group repeat/shuffle.
+ *
+ * Applies to {@link #SETTING_REPEAT}, {@link #SETTING_SHUFFLE} and {@link #SETTING_SCAN}.
+ */
+ public static final int STATE_GROUP = 0x04;
+
+ /**
+ * List of supported settings ORed.
+ */
+ private int mSettings;
+
+ /**
+ * Hash map of current capability values.
+ */
+ private Map<Integer, Integer> mSettingsValue = new HashMap<Integer, Integer>();
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSettings);
+ out.writeInt(mSettingsValue.size());
+ for (int k : mSettingsValue.keySet()) {
+ out.writeInt(k);
+ out.writeInt(mSettingsValue.get(k));
+ }
+ }
+
+ public static final @NonNull Creator<BluetoothAvrcpPlayerSettings> CREATOR = new Creator<>() {
+ public BluetoothAvrcpPlayerSettings createFromParcel(Parcel in) {
+ return new BluetoothAvrcpPlayerSettings(in);
+ }
+
+ public BluetoothAvrcpPlayerSettings[] newArray(int size) {
+ return new BluetoothAvrcpPlayerSettings[size];
+ }
+ };
+
+ private BluetoothAvrcpPlayerSettings(Parcel in) {
+ mSettings = in.readInt();
+ int numSettings = in.readInt();
+ for (int i = 0; i < numSettings; i++) {
+ mSettingsValue.put(in.readInt(), in.readInt());
+ }
+ }
+
+ /**
+ * Create a new player settings object.
+ *
+ * @param settings a ORed value of SETTINGS_* defined above.
+ */
+ public BluetoothAvrcpPlayerSettings(int settings) {
+ mSettings = settings;
+ }
+
+ /**
+ * Get the supported settings.
+ *
+ * @return int ORed value of supported settings.
+ */
+ public int getSettings() {
+ return mSettings;
+ }
+
+ /**
+ * Add a setting value.
+ *
+ * The setting must be part of possible settings in {@link getSettings()}.
+ *
+ * @param setting setting config.
+ * @param value value for the setting.
+ * @throws IllegalStateException if the setting is not supported.
+ */
+ public void addSettingValue(int setting, int value) {
+ if ((setting & mSettings) == 0) {
+ Log.e(TAG, "Setting not supported: " + setting + " " + mSettings);
+ throw new IllegalStateException("Setting not supported: " + setting);
+ }
+ mSettingsValue.put(setting, value);
+ }
+
+ /**
+ * Get a setting value.
+ *
+ * The setting must be part of possible settings in {@link getSettings()}.
+ *
+ * @param setting setting config.
+ * @return value value for the setting.
+ * @throws IllegalStateException if the setting is not supported.
+ */
+ public int getSettingValue(int setting) {
+ if ((setting & mSettings) == 0) {
+ Log.e(TAG, "Setting not supported: " + setting + " " + mSettings);
+ throw new IllegalStateException("Setting not supported: " + setting);
+ }
+ Integer i = mSettingsValue.get(setting);
+ if (i == null) return -1;
+ return i;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothClass.java b/android-34/android/bluetooth/BluetoothClass.java
new file mode 100644
index 0000000..9d8c5b7
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothClass.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a Bluetooth class, which describes general characteristics
+ * and capabilities of a device. For example, a Bluetooth class will
+ * specify the general device type such as a phone, a computer, or
+ * headset, and whether it's capable of services such as audio or telephony.
+ *
+ * <p>Every Bluetooth class is composed of zero or more service classes, and
+ * exactly one device class. The device class is further broken down into major
+ * and minor device class components.
+ *
+ * <p>{@link BluetoothClass} is useful as a hint to roughly describe a device
+ * (for example to show an icon in the UI), but does not reliably describe which
+ * Bluetooth profiles or services are actually supported by a device. Accurate
+ * service discovery is done through SDP requests, which are automatically
+ * performed when creating an RFCOMM socket with {@link
+ * BluetoothDevice#createRfcommSocketToServiceRecord} and {@link
+ * BluetoothAdapter#listenUsingRfcommWithServiceRecord}</p>
+ *
+ * <p>Use {@link BluetoothDevice#getBluetoothClass} to retrieve the class for
+ * a remote device.
+ *
+ * <!--
+ * The Bluetooth class is a 32 bit field. The format of these bits is defined at
+ * http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm
+ * (login required). This class contains that 32 bit field, and provides
+ * constants and methods to determine which Service Class(es) and Device Class
+ * are encoded in that field.
+ * -->
+ */
+public final class BluetoothClass implements Parcelable {
+ /**
+ * Legacy error value. Applications should use null instead.
+ *
+ * @hide
+ */
+ public static final int ERROR = 0xFF000000;
+
+ private final int mClass;
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public BluetoothClass(int classInt) {
+ mClass = classInt;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothClass) {
+ return mClass == ((BluetoothClass) o).mClass;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mClass;
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toHexString(mClass);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothClass> CREATOR =
+ new Parcelable.Creator<BluetoothClass>() {
+ public BluetoothClass createFromParcel(Parcel in) {
+ return new BluetoothClass(in.readInt());
+ }
+
+ public BluetoothClass[] newArray(int size) {
+ return new BluetoothClass[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mClass);
+ }
+
+ /**
+ * Defines all service class constants.
+ * <p>Each {@link BluetoothClass} encodes zero or more service classes.
+ */
+ public static final class Service {
+ private static final int BITMASK = 0xFFE000;
+
+ public static final int LIMITED_DISCOVERABILITY = 0x002000;
+ /** Represent devices LE audio service */
+ public static final int LE_AUDIO = 0x004000;
+ public static final int POSITIONING = 0x010000;
+ public static final int NETWORKING = 0x020000;
+ public static final int RENDER = 0x040000;
+ public static final int CAPTURE = 0x080000;
+ public static final int OBJECT_TRANSFER = 0x100000;
+ public static final int AUDIO = 0x200000;
+ public static final int TELEPHONY = 0x400000;
+ public static final int INFORMATION = 0x800000;
+ }
+
+ /**
+ * Return true if the specified service class is supported by this
+ * {@link BluetoothClass}.
+ * <p>Valid service classes are the public constants in
+ * {@link BluetoothClass.Service}. For example, {@link
+ * BluetoothClass.Service#AUDIO}.
+ *
+ * @param service valid service class
+ * @return true if the service class is supported
+ */
+ public boolean hasService(int service) {
+ return ((mClass & Service.BITMASK & service) != 0);
+ }
+
+ /**
+ * Defines all device class constants.
+ * <p>Each {@link BluetoothClass} encodes exactly one device class, with
+ * major and minor components.
+ * <p>The constants in {@link
+ * BluetoothClass.Device} represent a combination of major and minor
+ * device components (the complete device class). The constants in {@link
+ * BluetoothClass.Device.Major} represent only major device classes.
+ * <p>See {@link BluetoothClass.Service} for service class constants.
+ */
+ public static class Device {
+ private static final int BITMASK = 0x1FFC;
+
+ /**
+ * Defines all major device class constants.
+ * <p>See {@link BluetoothClass.Device} for minor classes.
+ */
+ public static class Major {
+ private static final int BITMASK = 0x1F00;
+
+ public static final int MISC = 0x0000;
+ public static final int COMPUTER = 0x0100;
+ public static final int PHONE = 0x0200;
+ public static final int NETWORKING = 0x0300;
+ public static final int AUDIO_VIDEO = 0x0400;
+ public static final int PERIPHERAL = 0x0500;
+ public static final int IMAGING = 0x0600;
+ public static final int WEARABLE = 0x0700;
+ public static final int TOY = 0x0800;
+ public static final int HEALTH = 0x0900;
+ public static final int UNCATEGORIZED = 0x1F00;
+ }
+
+ // Devices in the COMPUTER major class
+ public static final int COMPUTER_UNCATEGORIZED = 0x0100;
+ public static final int COMPUTER_DESKTOP = 0x0104;
+ public static final int COMPUTER_SERVER = 0x0108;
+ public static final int COMPUTER_LAPTOP = 0x010C;
+ public static final int COMPUTER_HANDHELD_PC_PDA = 0x0110;
+ public static final int COMPUTER_PALM_SIZE_PC_PDA = 0x0114;
+ public static final int COMPUTER_WEARABLE = 0x0118;
+
+ // Devices in the PHONE major class
+ public static final int PHONE_UNCATEGORIZED = 0x0200;
+ public static final int PHONE_CELLULAR = 0x0204;
+ public static final int PHONE_CORDLESS = 0x0208;
+ public static final int PHONE_SMART = 0x020C;
+ public static final int PHONE_MODEM_OR_GATEWAY = 0x0210;
+ public static final int PHONE_ISDN = 0x0214;
+
+ // Minor classes for the AUDIO_VIDEO major class
+ public static final int AUDIO_VIDEO_UNCATEGORIZED = 0x0400;
+ public static final int AUDIO_VIDEO_WEARABLE_HEADSET = 0x0404;
+ public static final int AUDIO_VIDEO_HANDSFREE = 0x0408;
+ //public static final int AUDIO_VIDEO_RESERVED = 0x040C;
+ public static final int AUDIO_VIDEO_MICROPHONE = 0x0410;
+ public static final int AUDIO_VIDEO_LOUDSPEAKER = 0x0414;
+ public static final int AUDIO_VIDEO_HEADPHONES = 0x0418;
+ public static final int AUDIO_VIDEO_PORTABLE_AUDIO = 0x041C;
+ public static final int AUDIO_VIDEO_CAR_AUDIO = 0x0420;
+ public static final int AUDIO_VIDEO_SET_TOP_BOX = 0x0424;
+ public static final int AUDIO_VIDEO_HIFI_AUDIO = 0x0428;
+ public static final int AUDIO_VIDEO_VCR = 0x042C;
+ public static final int AUDIO_VIDEO_VIDEO_CAMERA = 0x0430;
+ public static final int AUDIO_VIDEO_CAMCORDER = 0x0434;
+ public static final int AUDIO_VIDEO_VIDEO_MONITOR = 0x0438;
+ public static final int AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x043C;
+ public static final int AUDIO_VIDEO_VIDEO_CONFERENCING = 0x0440;
+ //public static final int AUDIO_VIDEO_RESERVED = 0x0444;
+ public static final int AUDIO_VIDEO_VIDEO_GAMING_TOY = 0x0448;
+
+ // Devices in the WEARABLE major class
+ public static final int WEARABLE_UNCATEGORIZED = 0x0700;
+ public static final int WEARABLE_WRIST_WATCH = 0x0704;
+ public static final int WEARABLE_PAGER = 0x0708;
+ public static final int WEARABLE_JACKET = 0x070C;
+ public static final int WEARABLE_HELMET = 0x0710;
+ public static final int WEARABLE_GLASSES = 0x0714;
+
+ // Devices in the TOY major class
+ public static final int TOY_UNCATEGORIZED = 0x0800;
+ public static final int TOY_ROBOT = 0x0804;
+ public static final int TOY_VEHICLE = 0x0808;
+ public static final int TOY_DOLL_ACTION_FIGURE = 0x080C;
+ public static final int TOY_CONTROLLER = 0x0810;
+ public static final int TOY_GAME = 0x0814;
+
+ // Devices in the HEALTH major class
+ public static final int HEALTH_UNCATEGORIZED = 0x0900;
+ public static final int HEALTH_BLOOD_PRESSURE = 0x0904;
+ public static final int HEALTH_THERMOMETER = 0x0908;
+ public static final int HEALTH_WEIGHING = 0x090C;
+ public static final int HEALTH_GLUCOSE = 0x0910;
+ public static final int HEALTH_PULSE_OXIMETER = 0x0914;
+ public static final int HEALTH_PULSE_RATE = 0x0918;
+ public static final int HEALTH_DATA_DISPLAY = 0x091C;
+
+ // Devices in PERIPHERAL major class
+ public static final int PERIPHERAL_NON_KEYBOARD_NON_POINTING = 0x0500;
+ public static final int PERIPHERAL_KEYBOARD = 0x0540;
+ public static final int PERIPHERAL_POINTING = 0x0580;
+ public static final int PERIPHERAL_KEYBOARD_POINTING = 0x05C0;
+ }
+
+ /**
+ * Return the major device class component of this {@link BluetoothClass}.
+ * <p>Values returned from this function can be compared with the
+ * public constants in {@link BluetoothClass.Device.Major} to determine
+ * which major class is encoded in this Bluetooth class.
+ *
+ * @return major device class component
+ */
+ public int getMajorDeviceClass() {
+ return (mClass & Device.Major.BITMASK);
+ }
+
+ /**
+ * Return the (major and minor) device class component of this
+ * {@link BluetoothClass}.
+ * <p>Values returned from this function can be compared with the
+ * public constants in {@link BluetoothClass.Device} to determine which
+ * device class is encoded in this Bluetooth class.
+ *
+ * @return device class component
+ */
+ public int getDeviceClass() {
+ return (mClass & Device.BITMASK);
+ }
+
+ /**
+ * Return the Bluetooth Class of Device (CoD) value including the
+ * {@link BluetoothClass.Service}, {@link BluetoothClass.Device.Major} and
+ * minor device fields.
+ *
+ * <p>This value is an integer representation of Bluetooth CoD as in
+ * Bluetooth specification.
+ *
+ * @see <a href="Bluetooth CoD">https://www.bluetooth.com/specifications/assigned-numbers/baseband</a>
+ *
+ * @hide
+ */
+ public int getClassOfDevice() {
+ return mClass;
+ }
+
+ public static final int PROFILE_HEADSET = 0;
+
+ public static final int PROFILE_A2DP = 1;
+
+ /** @hide */
+ @SystemApi
+ public static final int PROFILE_OPP = 2;
+
+ public static final int PROFILE_HID = 3;
+
+ /** @hide */
+ @SystemApi
+ public static final int PROFILE_PANU = 4;
+
+ /** @hide */
+ @SystemApi
+ public static final int PROFILE_NAP = 5;
+
+ /** @hide */
+ @SystemApi
+ public static final int PROFILE_A2DP_SINK = 6;
+
+ /**
+ * Check class bits for possible bluetooth profile support.
+ * This is a simple heuristic that tries to guess if a device with the
+ * given class bits might support specified profile. It is not accurate for all
+ * devices. It tries to err on the side of false positives.
+ *
+ * @param profile the profile to be checked
+ * @return whether this device supports specified profile
+ */
+ public boolean doesClassMatch(int profile) {
+ if (profile == PROFILE_A2DP) {
+ if (hasService(Service.RENDER)) {
+ return true;
+ }
+ // By the A2DP spec, sinks must indicate the RENDER service.
+ // However we found some that do not (Chordette). So lets also
+ // match on some other class bits.
+ switch (getDeviceClass()) {
+ case Device.AUDIO_VIDEO_HIFI_AUDIO:
+ case Device.AUDIO_VIDEO_HEADPHONES:
+ case Device.AUDIO_VIDEO_LOUDSPEAKER:
+ case Device.AUDIO_VIDEO_CAR_AUDIO:
+ return true;
+ default:
+ return false;
+ }
+ } else if (profile == PROFILE_A2DP_SINK) {
+ if (hasService(Service.CAPTURE)) {
+ return true;
+ }
+ // By the A2DP spec, srcs must indicate the CAPTURE service.
+ // However if some device that do not, we try to
+ // match on some other class bits.
+ switch (getDeviceClass()) {
+ case Device.AUDIO_VIDEO_HIFI_AUDIO:
+ case Device.AUDIO_VIDEO_SET_TOP_BOX:
+ case Device.AUDIO_VIDEO_VCR:
+ return true;
+ default:
+ return false;
+ }
+ } else if (profile == PROFILE_HEADSET) {
+ // The render service class is required by the spec for HFP, so is a
+ // pretty good signal
+ if (hasService(Service.RENDER)) {
+ return true;
+ }
+ // Just in case they forgot the render service class
+ switch (getDeviceClass()) {
+ case Device.AUDIO_VIDEO_HANDSFREE:
+ case Device.AUDIO_VIDEO_WEARABLE_HEADSET:
+ case Device.AUDIO_VIDEO_CAR_AUDIO:
+ return true;
+ default:
+ return false;
+ }
+ } else if (profile == PROFILE_OPP) {
+ if (hasService(Service.OBJECT_TRANSFER)) {
+ return true;
+ }
+
+ switch (getDeviceClass()) {
+ case Device.COMPUTER_UNCATEGORIZED:
+ case Device.COMPUTER_DESKTOP:
+ case Device.COMPUTER_SERVER:
+ case Device.COMPUTER_LAPTOP:
+ case Device.COMPUTER_HANDHELD_PC_PDA:
+ case Device.COMPUTER_PALM_SIZE_PC_PDA:
+ case Device.COMPUTER_WEARABLE:
+ case Device.PHONE_UNCATEGORIZED:
+ case Device.PHONE_CELLULAR:
+ case Device.PHONE_CORDLESS:
+ case Device.PHONE_SMART:
+ case Device.PHONE_MODEM_OR_GATEWAY:
+ case Device.PHONE_ISDN:
+ return true;
+ default:
+ return false;
+ }
+ } else if (profile == PROFILE_HID) {
+ return getMajorDeviceClass() == Device.Major.PERIPHERAL;
+ } else if (profile == PROFILE_PANU || profile == PROFILE_NAP) {
+ // No good way to distinguish between the two, based on class bits.
+ if (hasService(Service.NETWORKING)) {
+ return true;
+ }
+ return getMajorDeviceClass() == Device.Major.NETWORKING;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothCodecConfig.java b/android-34/android/bluetooth/BluetoothCodecConfig.java
new file mode 100644
index 0000000..e0ea232
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothCodecConfig.java
@@ -0,0 +1,863 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents the codec configuration for a Bluetooth A2DP source device.
+ * <p>Contains the source codec type, the codec priority, the codec sample
+ * rate, the codec bits per sample, and the codec channel mode.
+ * <p>The source codec type values are the same as those supported by the
+ * device hardware.
+ *
+ * {@see BluetoothA2dp}
+ */
+public final class BluetoothCodecConfig implements Parcelable {
+ /** @hide */
+ @IntDef(prefix = "SOURCE_CODEC_TYPE_",
+ value = {SOURCE_CODEC_TYPE_SBC, SOURCE_CODEC_TYPE_AAC, SOURCE_CODEC_TYPE_APTX,
+ SOURCE_CODEC_TYPE_APTX_HD, SOURCE_CODEC_TYPE_LDAC, SOURCE_CODEC_TYPE_LC3,
+ SOURCE_CODEC_TYPE_OPUS, SOURCE_CODEC_TYPE_INVALID})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SourceCodecType {}
+
+ /**
+ * Source codec type SBC. This is the mandatory source codec
+ * type.
+ */
+ public static final int SOURCE_CODEC_TYPE_SBC = 0;
+
+ /**
+ * Source codec type AAC.
+ */
+ public static final int SOURCE_CODEC_TYPE_AAC = 1;
+
+ /**
+ * Source codec type APTX.
+ */
+ public static final int SOURCE_CODEC_TYPE_APTX = 2;
+
+ /**
+ * Source codec type APTX HD.
+ */
+ public static final int SOURCE_CODEC_TYPE_APTX_HD = 3;
+
+ /**
+ * Source codec type LDAC.
+ */
+ public static final int SOURCE_CODEC_TYPE_LDAC = 4;
+
+ /**
+ * Source codec type LC3.
+ */
+ public static final int SOURCE_CODEC_TYPE_LC3 = 5;
+
+ /**
+ * Source codec type Opus.
+ */
+ public static final int SOURCE_CODEC_TYPE_OPUS = 6;
+
+ /**
+ * Source codec type invalid. This is the default value used for codec
+ * type.
+ */
+ public static final int SOURCE_CODEC_TYPE_INVALID = 1000 * 1000;
+
+ /**
+ * Represents the count of valid source codec types.
+ */
+ private static final int SOURCE_CODEC_TYPE_MAX = 7;
+
+ /** @hide */
+ @IntDef(prefix = "CODEC_PRIORITY_", value = {
+ CODEC_PRIORITY_DISABLED,
+ CODEC_PRIORITY_DEFAULT,
+ CODEC_PRIORITY_HIGHEST
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CodecPriority {}
+
+ /**
+ * Codec priority disabled.
+ * Used to indicate that this codec is disabled and should not be used.
+ */
+ public static final int CODEC_PRIORITY_DISABLED = -1;
+
+ /**
+ * Codec priority default.
+ * Default value used for codec priority.
+ */
+ public static final int CODEC_PRIORITY_DEFAULT = 0;
+
+ /**
+ * Codec priority highest.
+ * Used to indicate the highest priority a codec can have.
+ */
+ public static final int CODEC_PRIORITY_HIGHEST = 1000 * 1000;
+
+ /** @hide */
+ @IntDef(prefix = "SAMPLE_RATE_", value = {
+ SAMPLE_RATE_NONE,
+ SAMPLE_RATE_44100,
+ SAMPLE_RATE_48000,
+ SAMPLE_RATE_88200,
+ SAMPLE_RATE_96000,
+ SAMPLE_RATE_176400,
+ SAMPLE_RATE_192000
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SampleRate {}
+
+ /**
+ * Codec sample rate 0 Hz. Default value used for
+ * codec sample rate.
+ */
+ public static final int SAMPLE_RATE_NONE = 0;
+
+ /**
+ * Codec sample rate 44100 Hz.
+ */
+ public static final int SAMPLE_RATE_44100 = 0x1 << 0;
+
+ /**
+ * Codec sample rate 48000 Hz.
+ */
+ public static final int SAMPLE_RATE_48000 = 0x1 << 1;
+
+ /**
+ * Codec sample rate 88200 Hz.
+ */
+ public static final int SAMPLE_RATE_88200 = 0x1 << 2;
+
+ /**
+ * Codec sample rate 96000 Hz.
+ */
+ public static final int SAMPLE_RATE_96000 = 0x1 << 3;
+
+ /**
+ * Codec sample rate 176400 Hz.
+ */
+ public static final int SAMPLE_RATE_176400 = 0x1 << 4;
+
+ /**
+ * Codec sample rate 192000 Hz.
+ */
+ public static final int SAMPLE_RATE_192000 = 0x1 << 5;
+
+ /** @hide */
+ @IntDef(prefix = "BITS_PER_SAMPLE_", value = {
+ BITS_PER_SAMPLE_NONE,
+ BITS_PER_SAMPLE_16,
+ BITS_PER_SAMPLE_24,
+ BITS_PER_SAMPLE_32
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BitsPerSample {}
+
+ /**
+ * Codec bits per sample 0. Default value of the codec
+ * bits per sample.
+ */
+ public static final int BITS_PER_SAMPLE_NONE = 0;
+
+ /**
+ * Codec bits per sample 16.
+ */
+ public static final int BITS_PER_SAMPLE_16 = 0x1 << 0;
+
+ /**
+ * Codec bits per sample 24.
+ */
+ public static final int BITS_PER_SAMPLE_24 = 0x1 << 1;
+
+ /**
+ * Codec bits per sample 32.
+ */
+ public static final int BITS_PER_SAMPLE_32 = 0x1 << 2;
+
+ /** @hide */
+ @IntDef(prefix = "CHANNEL_MODE_", value = {
+ CHANNEL_MODE_NONE,
+ CHANNEL_MODE_MONO,
+ CHANNEL_MODE_STEREO
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ChannelMode {}
+
+ /**
+ * Codec channel mode NONE. Default value of the
+ * codec channel mode.
+ */
+ public static final int CHANNEL_MODE_NONE = 0;
+
+ /**
+ * Codec channel mode MONO.
+ */
+ public static final int CHANNEL_MODE_MONO = 0x1 << 0;
+
+ /**
+ * Codec channel mode STEREO.
+ */
+ public static final int CHANNEL_MODE_STEREO = 0x1 << 1;
+
+ private final @SourceCodecType int mCodecType;
+ private @CodecPriority int mCodecPriority;
+ private final @SampleRate int mSampleRate;
+ private final @BitsPerSample int mBitsPerSample;
+ private final @ChannelMode int mChannelMode;
+ private final long mCodecSpecific1;
+ private final long mCodecSpecific2;
+ private final long mCodecSpecific3;
+ private final long mCodecSpecific4;
+
+ /**
+ * Creates a new BluetoothCodecConfig.
+ *
+ * @param codecType the source codec type
+ * @param codecPriority the priority of this codec
+ * @param sampleRate the codec sample rate
+ * @param bitsPerSample the bits per sample of this codec
+ * @param channelMode the channel mode of this codec
+ * @param codecSpecific1 the specific value 1
+ * @param codecSpecific2 the specific value 2
+ * @param codecSpecific3 the specific value 3
+ * @param codecSpecific4 the specific value 4
+ * values to 0.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public BluetoothCodecConfig(@SourceCodecType int codecType, @CodecPriority int codecPriority,
+ @SampleRate int sampleRate, @BitsPerSample int bitsPerSample,
+ @ChannelMode int channelMode, long codecSpecific1,
+ long codecSpecific2, long codecSpecific3,
+ long codecSpecific4) {
+ mCodecType = codecType;
+ mCodecPriority = codecPriority;
+ mSampleRate = sampleRate;
+ mBitsPerSample = bitsPerSample;
+ mChannelMode = channelMode;
+ mCodecSpecific1 = codecSpecific1;
+ mCodecSpecific2 = codecSpecific2;
+ mCodecSpecific3 = codecSpecific3;
+ mCodecSpecific4 = codecSpecific4;
+ }
+
+ /**
+ * Creates a new BluetoothCodecConfig.
+ * <p> By default, the codec priority will be set
+ * to {@link BluetoothCodecConfig#CODEC_PRIORITY_DEFAULT}, the sample rate to
+ * {@link BluetoothCodecConfig#SAMPLE_RATE_NONE}, the bits per sample to
+ * {@link BluetoothCodecConfig#BITS_PER_SAMPLE_NONE}, the channel mode to
+ * {@link BluetoothCodecConfig#CHANNEL_MODE_NONE}, and all the codec specific
+ * values to 0.
+ *
+ * @param codecType the source codec type
+ * @hide
+ */
+ public BluetoothCodecConfig(@SourceCodecType int codecType) {
+ this(codecType, BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_NONE,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+ BluetoothCodecConfig.CHANNEL_MODE_NONE, 0, 0, 0, 0);
+ }
+
+ private BluetoothCodecConfig(Parcel in) {
+ mCodecType = in.readInt();
+ mCodecPriority = in.readInt();
+ mSampleRate = in.readInt();
+ mBitsPerSample = in.readInt();
+ mChannelMode = in.readInt();
+ mCodecSpecific1 = in.readLong();
+ mCodecSpecific2 = in.readLong();
+ mCodecSpecific3 = in.readLong();
+ mCodecSpecific4 = in.readLong();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothCodecConfig) {
+ BluetoothCodecConfig other = (BluetoothCodecConfig) o;
+ return (other.mCodecType == mCodecType
+ && other.mCodecPriority == mCodecPriority
+ && other.mSampleRate == mSampleRate
+ && other.mBitsPerSample == mBitsPerSample
+ && other.mChannelMode == mChannelMode
+ && other.mCodecSpecific1 == mCodecSpecific1
+ && other.mCodecSpecific2 == mCodecSpecific2
+ && other.mCodecSpecific3 == mCodecSpecific3
+ && other.mCodecSpecific4 == mCodecSpecific4);
+ }
+ return false;
+ }
+
+ /**
+ * Returns a hash representation of this BluetoothCodecConfig
+ * based on all the config values.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCodecType, mCodecPriority, mSampleRate,
+ mBitsPerSample, mChannelMode, mCodecSpecific1,
+ mCodecSpecific2, mCodecSpecific3, mCodecSpecific4);
+ }
+
+ /**
+ * Adds capability string to an existing string.
+ *
+ * @param prevStr the previous string with the capabilities. Can be a {@code null} pointer
+ * @param capStr the capability string to append to prevStr argument
+ * @return the result string in the form "prevStr|capStr"
+ */
+ private static String appendCapabilityToString(@Nullable String prevStr,
+ @NonNull String capStr) {
+ if (prevStr == null) {
+ return capStr;
+ }
+ return prevStr + "|" + capStr;
+ }
+
+ /**
+ * Returns a {@link String} that describes each BluetoothCodecConfig parameter
+ * current value.
+ */
+ @Override
+ public String toString() {
+ String sampleRateStr = null;
+ if (mSampleRate == SAMPLE_RATE_NONE) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "NONE");
+ }
+ if ((mSampleRate & SAMPLE_RATE_44100) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "44100");
+ }
+ if ((mSampleRate & SAMPLE_RATE_48000) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "48000");
+ }
+ if ((mSampleRate & SAMPLE_RATE_88200) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "88200");
+ }
+ if ((mSampleRate & SAMPLE_RATE_96000) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "96000");
+ }
+ if ((mSampleRate & SAMPLE_RATE_176400) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "176400");
+ }
+ if ((mSampleRate & SAMPLE_RATE_192000) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "192000");
+ }
+
+ String bitsPerSampleStr = null;
+ if (mBitsPerSample == BITS_PER_SAMPLE_NONE) {
+ bitsPerSampleStr = appendCapabilityToString(bitsPerSampleStr, "NONE");
+ }
+ if ((mBitsPerSample & BITS_PER_SAMPLE_16) != 0) {
+ bitsPerSampleStr = appendCapabilityToString(bitsPerSampleStr, "16");
+ }
+ if ((mBitsPerSample & BITS_PER_SAMPLE_24) != 0) {
+ bitsPerSampleStr = appendCapabilityToString(bitsPerSampleStr, "24");
+ }
+ if ((mBitsPerSample & BITS_PER_SAMPLE_32) != 0) {
+ bitsPerSampleStr = appendCapabilityToString(bitsPerSampleStr, "32");
+ }
+
+ String channelModeStr = null;
+ if (mChannelMode == CHANNEL_MODE_NONE) {
+ channelModeStr = appendCapabilityToString(channelModeStr, "NONE");
+ }
+ if ((mChannelMode & CHANNEL_MODE_MONO) != 0) {
+ channelModeStr = appendCapabilityToString(channelModeStr, "MONO");
+ }
+ if ((mChannelMode & CHANNEL_MODE_STEREO) != 0) {
+ channelModeStr = appendCapabilityToString(channelModeStr, "STEREO");
+ }
+
+ return "{codecName:" + getCodecName(mCodecType)
+ + ",mCodecType:" + mCodecType
+ + ",mCodecPriority:" + mCodecPriority
+ + ",mSampleRate:" + String.format("0x%x", mSampleRate)
+ + "(" + sampleRateStr + ")"
+ + ",mBitsPerSample:" + String.format("0x%x", mBitsPerSample)
+ + "(" + bitsPerSampleStr + ")"
+ + ",mChannelMode:" + String.format("0x%x", mChannelMode)
+ + "(" + channelModeStr + ")"
+ + ",mCodecSpecific1:" + mCodecSpecific1
+ + ",mCodecSpecific2:" + mCodecSpecific2
+ + ",mCodecSpecific3:" + mCodecSpecific3
+ + ",mCodecSpecific4:" + mCodecSpecific4 + "}";
+ }
+
+ /**
+ * @return 0
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<BluetoothCodecConfig> CREATOR = new Creator<>() {
+ public BluetoothCodecConfig createFromParcel(Parcel in) {
+ return new BluetoothCodecConfig(in);
+ }
+
+ public BluetoothCodecConfig[] newArray(int size) {
+ return new BluetoothCodecConfig[size];
+ }
+ };
+
+ /**
+ * Flattens the object to a parcel
+ *
+ * @param out The Parcel in which the object should be written
+ * @param flags Additional flags about how the object should be written
+ *
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mCodecType);
+ out.writeInt(mCodecPriority);
+ out.writeInt(mSampleRate);
+ out.writeInt(mBitsPerSample);
+ out.writeInt(mChannelMode);
+ out.writeLong(mCodecSpecific1);
+ out.writeLong(mCodecSpecific2);
+ out.writeLong(mCodecSpecific3);
+ out.writeLong(mCodecSpecific4);
+ }
+
+ /**
+ * Returns the codec name converted to {@link String}.
+ * @hide
+ */
+ public static @NonNull String getCodecName(@SourceCodecType int codecType) {
+ switch (codecType) {
+ case SOURCE_CODEC_TYPE_SBC:
+ return "SBC";
+ case SOURCE_CODEC_TYPE_AAC:
+ return "AAC";
+ case SOURCE_CODEC_TYPE_APTX:
+ return "aptX";
+ case SOURCE_CODEC_TYPE_APTX_HD:
+ return "aptX HD";
+ case SOURCE_CODEC_TYPE_LDAC:
+ return "LDAC";
+ case SOURCE_CODEC_TYPE_LC3:
+ return "LC3";
+ case SOURCE_CODEC_TYPE_OPUS:
+ return "Opus";
+ case SOURCE_CODEC_TYPE_INVALID:
+ return "INVALID CODEC";
+ default:
+ break;
+ }
+ return "UNKNOWN CODEC(" + codecType + ")";
+ }
+
+ /**
+ * Returns the source codec type of this config.
+ */
+ public @SourceCodecType int getCodecType() {
+ return mCodecType;
+ }
+
+ /**
+ * Checks whether the codec is mandatory.
+ * <p> The actual mandatory codec type for Android Bluetooth audio is SBC.
+ * See {@link #SOURCE_CODEC_TYPE_SBC}.
+ *
+ * @return {@code true} if the codec is mandatory, {@code false} otherwise
+ */
+ public boolean isMandatoryCodec() {
+ return mCodecType == SOURCE_CODEC_TYPE_SBC;
+ }
+
+ /**
+ * Returns the codec selection priority.
+ * <p>The codec selection priority is relative to other codecs: larger value
+ * means higher priority.
+ */
+ public @CodecPriority int getCodecPriority() {
+ return mCodecPriority;
+ }
+
+ /**
+ * Sets the codec selection priority.
+ * <p>The codec selection priority is relative to other codecs: larger value
+ * means higher priority.
+ *
+ * @param codecPriority the priority this codec should have
+ * @hide
+ */
+ public void setCodecPriority(@CodecPriority int codecPriority) {
+ mCodecPriority = codecPriority;
+ }
+
+ /**
+ * Returns the codec sample rate. The value can be a bitmask with all
+ * supported sample rates.
+ */
+ public @SampleRate int getSampleRate() {
+ return mSampleRate;
+ }
+
+ /**
+ * Returns the codec bits per sample. The value can be a bitmask with all
+ * bits per sample supported.
+ */
+ public @BitsPerSample int getBitsPerSample() {
+ return mBitsPerSample;
+ }
+
+ /**
+ * Returns the codec channel mode. The value can be a bitmask with all
+ * supported channel modes.
+ */
+ public @ChannelMode int getChannelMode() {
+ return mChannelMode;
+ }
+
+ /**
+ * Returns the codec specific value1.
+ * As the value and usage differ for each codec, please refer to the concerned
+ * codec specification to obtain the codec specific information.
+ *
+ * <p>See section 4.3.2 of the Bluetooth A2dp specification for SBC codec specific
+ * information elements.
+ * <p>See section 4.4.2 of the Bluetooth A2dp specification for MPEG-1,2 Audio
+ * codec specific information elements.
+ * <p>See section 4.5.2 of the Bluetooth A2dp specification for MPEG-2, 4 AAC
+ * codec specific information elements.
+ * <p>See section 4.6.2 of the Bluetooth A2dp specification for ATRAC family
+ * codec specific information elements.
+ * <p>See section 4.7.2 of the Bluetooth A2dp specification for Vendor Specific A2DP
+ * codec specific information elements.
+ */
+ public long getCodecSpecific1() {
+ return mCodecSpecific1;
+ }
+
+ /**
+ * Returns the codec specific value2.
+ * As the value and usage differ for each codec, please refer to the concerned
+ * codec specification to obtain the codec specific information.
+ *
+ * <p>See section 4.3.2 of the Bluetooth A2dp specification for SBC codec specific
+ * information elements.
+ * <p>See section 4.4.2 of the Bluetooth A2dp specification for MPEG-1,2 Audio
+ * codec specific information elements.
+ * <p>See section 4.5.2 of the Bluetooth A2dp specification for MPEG-2, 4 AAC
+ * codec specific information elements.
+ * <p>See section 4.6.2 of the Bluetooth A2dp specification for ATRAC family
+ * codec specific information elements.
+ * <p>See section 4.7.2 of the Bluetooth A2dp specification for Vendor Specific A2DP
+ * codec specific information elements.
+ */
+ public long getCodecSpecific2() {
+ return mCodecSpecific2;
+ }
+
+ /**
+ * Returns the codec specific value3.
+ * As the value and usage differ for each codec, please refer to the concerned
+ * codec specification to obtain the codec specific information.
+ *
+ * <p>See section 4.3.2 of the Bluetooth A2dp specification for SBC codec specific
+ * information elements.
+ * <p>See section 4.4.2 of the Bluetooth A2dp specification for MPEG-1,2 Audio
+ * codec specific information elements.
+ * <p>See section 4.5.2 of the Bluetooth A2dp specification for MPEG-2, 4 AAC
+ * codec specific information elements.
+ * <p>See section 4.6.2 of the Bluetooth A2dp specification for ATRAC family
+ * codec specific information elements.
+ * <p>See section 4.7.2 of the Bluetooth A2dp specification for Vendor Specific A2DP
+ * codec specific information elements.
+ */
+ public long getCodecSpecific3() {
+ return mCodecSpecific3;
+ }
+
+ /**
+ * Returns the codec specific value4.
+ * As the value and usage differ for each codec, please refer to the concerned
+ * codec specification to obtain the codec specific information.
+ *
+ * <p>See section 4.3.2 of the Bluetooth A2dp specification for SBC codec specific
+ * information elements.
+ * <p>See section 4.4.2 of the Bluetooth A2dp specification for MPEG-1,2 Audio
+ * codec specific information elements.
+ * <p>See section 4.5.2 of the Bluetooth A2dp specification for MPEG-2, 4 AAC
+ * codec specific information elements.
+ * <p>See section 4.6.2 of the Bluetooth A2dp specification for ATRAC family
+ * codec specific information elements.
+ * <p>See section 4.7.2 of the Bluetooth A2dp specification for Vendor Specific A2DP
+ * codec specific information elements.
+ */
+ public long getCodecSpecific4() {
+ return mCodecSpecific4;
+ }
+
+ /**
+ * Checks whether a value set presented by a bitmask has zero or single bit
+ *
+ * @param valueSet the value set presented by a bitmask
+ * @return {@code true} if the valueSet contains zero or single bit, {@code false} otherwise
+ * @hide
+ */
+ private static boolean hasSingleBit(int valueSet) {
+ return (valueSet == 0 || (valueSet & (valueSet - 1)) == 0);
+ }
+
+ /**
+ * Returns whether the object contains none or single sample rate.
+ * @hide
+ */
+ public boolean hasSingleSampleRate() {
+ return hasSingleBit(mSampleRate);
+ }
+
+ /**
+ * Returns whether the object contains none or single bits per sample.
+ * @hide
+ */
+ public boolean hasSingleBitsPerSample() {
+ return hasSingleBit(mBitsPerSample);
+ }
+
+ /**
+ * Returns whether the object contains none or single channel mode.
+ * @hide
+ */
+ public boolean hasSingleChannelMode() {
+ return hasSingleBit(mChannelMode);
+ }
+
+ /**
+ * Checks whether the audio feeding parameters are the same.
+ *
+ * @param other the codec config to compare against
+ * @return {@code true} if the audio feeding parameters are same, {@code false} otherwise
+ * @hide
+ */
+ public boolean sameAudioFeedingParameters(BluetoothCodecConfig other) {
+ return (other != null && other.mSampleRate == mSampleRate
+ && other.mBitsPerSample == mBitsPerSample
+ && other.mChannelMode == mChannelMode);
+ }
+
+ /**
+ * Checks whether another codec config has the similar feeding parameters.
+ * Any parameters with NONE value will be considered to be a wildcard matching.
+ *
+ * @param other the codec config to compare against
+ * @return {@code true} if the audio feeding parameters are similar, {@code false} otherwise
+ * @hide
+ */
+ public boolean similarCodecFeedingParameters(BluetoothCodecConfig other) {
+ if (other == null || mCodecType != other.mCodecType) {
+ return false;
+ }
+ int sampleRate = other.mSampleRate;
+ if (mSampleRate == SAMPLE_RATE_NONE
+ || sampleRate == SAMPLE_RATE_NONE) {
+ sampleRate = mSampleRate;
+ }
+ int bitsPerSample = other.mBitsPerSample;
+ if (mBitsPerSample == BITS_PER_SAMPLE_NONE
+ || bitsPerSample == BITS_PER_SAMPLE_NONE) {
+ bitsPerSample = mBitsPerSample;
+ }
+ int channelMode = other.mChannelMode;
+ if (mChannelMode == CHANNEL_MODE_NONE
+ || channelMode == CHANNEL_MODE_NONE) {
+ channelMode = mChannelMode;
+ }
+ return sameAudioFeedingParameters(new BluetoothCodecConfig(
+ mCodecType, /* priority */ 0, sampleRate, bitsPerSample, channelMode,
+ /* specific1 */ 0, /* specific2 */ 0, /* specific3 */ 0,
+ /* specific4 */ 0));
+ }
+
+ /**
+ * Checks whether the codec specific parameters are the same.
+ * <p> Currently, only AAC VBR and LDAC Playback Quality on CodecSpecific1
+ * are compared.
+ *
+ * @param other the codec config to compare against
+ * @return {@code true} if the codec specific parameters are the same, {@code false} otherwise
+ * @hide
+ */
+ public boolean sameCodecSpecificParameters(BluetoothCodecConfig other) {
+ if (other == null && mCodecType != other.mCodecType) {
+ return false;
+ }
+ switch (mCodecType) {
+ case SOURCE_CODEC_TYPE_AAC:
+ case SOURCE_CODEC_TYPE_LDAC:
+ case SOURCE_CODEC_TYPE_LC3:
+ case SOURCE_CODEC_TYPE_OPUS:
+ if (mCodecSpecific1 != other.mCodecSpecific1) {
+ return false;
+ }
+ // fall through
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Builder for {@link BluetoothCodecConfig}.
+ * <p> By default, the codec type will be set to
+ * {@link BluetoothCodecConfig#SOURCE_CODEC_TYPE_INVALID}, the codec priority
+ * to {@link BluetoothCodecConfig#CODEC_PRIORITY_DEFAULT}, the sample rate to
+ * {@link BluetoothCodecConfig#SAMPLE_RATE_NONE}, the bits per sample to
+ * {@link BluetoothCodecConfig#BITS_PER_SAMPLE_NONE}, the channel mode to
+ * {@link BluetoothCodecConfig#CHANNEL_MODE_NONE}, and all the codec specific
+ * values to 0.
+ */
+ public static final class Builder {
+ private int mCodecType = BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID;
+ private int mCodecPriority = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
+ private int mSampleRate = BluetoothCodecConfig.SAMPLE_RATE_NONE;
+ private int mBitsPerSample = BluetoothCodecConfig.BITS_PER_SAMPLE_NONE;
+ private int mChannelMode = BluetoothCodecConfig.CHANNEL_MODE_NONE;
+ private long mCodecSpecific1 = 0;
+ private long mCodecSpecific2 = 0;
+ private long mCodecSpecific3 = 0;
+ private long mCodecSpecific4 = 0;
+
+ /**
+ * Set codec type for Bluetooth codec config.
+ *
+ * @param codecType of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecType(@SourceCodecType int codecType) {
+ mCodecType = codecType;
+ return this;
+ }
+
+ /**
+ * Set codec priority for Bluetooth codec config.
+ *
+ * @param codecPriority of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecPriority(@CodecPriority int codecPriority) {
+ mCodecPriority = codecPriority;
+ return this;
+ }
+
+ /**
+ * Set sample rate for Bluetooth codec config.
+ *
+ * @param sampleRate of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setSampleRate(@SampleRate int sampleRate) {
+ mSampleRate = sampleRate;
+ return this;
+ }
+
+ /**
+ * Set the bits per sample for Bluetooth codec config.
+ *
+ * @param bitsPerSample of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setBitsPerSample(@BitsPerSample int bitsPerSample) {
+ mBitsPerSample = bitsPerSample;
+ return this;
+ }
+
+ /**
+ * Set the channel mode for Bluetooth codec config.
+ *
+ * @param channelMode of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setChannelMode(@ChannelMode int channelMode) {
+ mChannelMode = channelMode;
+ return this;
+ }
+
+ /**
+ * Set the first codec specific values for Bluetooth codec config.
+ *
+ * @param codecSpecific1 codec specific value or 0 if default
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecSpecific1(long codecSpecific1) {
+ mCodecSpecific1 = codecSpecific1;
+ return this;
+ }
+
+ /**
+ * Set the second codec specific values for Bluetooth codec config.
+ *
+ * @param codecSpecific2 codec specific value or 0 if default
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecSpecific2(long codecSpecific2) {
+ mCodecSpecific2 = codecSpecific2;
+ return this;
+ }
+
+ /**
+ * Set the third codec specific values for Bluetooth codec config.
+ *
+ * @param codecSpecific3 codec specific value or 0 if default
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecSpecific3(long codecSpecific3) {
+ mCodecSpecific3 = codecSpecific3;
+ return this;
+ }
+
+ /**
+ * Set the fourth codec specific values for Bluetooth codec config.
+ *
+ * @param codecSpecific4 codec specific value or 0 if default
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecSpecific4(long codecSpecific4) {
+ mCodecSpecific4 = codecSpecific4;
+ return this;
+ }
+
+ /**
+ * Build {@link BluetoothCodecConfig}.
+ * @return new BluetoothCodecConfig built
+ */
+ public @NonNull BluetoothCodecConfig build() {
+ return new BluetoothCodecConfig(mCodecType, mCodecPriority,
+ mSampleRate, mBitsPerSample,
+ mChannelMode, mCodecSpecific1,
+ mCodecSpecific2, mCodecSpecific3,
+ mCodecSpecific4);
+ }
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothCodecStatus.java b/android-34/android/bluetooth/BluetoothCodecStatus.java
new file mode 100644
index 0000000..bd35806
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothCodecStatus.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents the codec status (configuration and capability) for a Bluetooth
+ * A2DP source device.
+ *
+ * {@see BluetoothA2dp}
+ */
+public final class BluetoothCodecStatus implements Parcelable {
+ /**
+ * Extra for the codec configuration intents of the individual profiles.
+ *
+ * This extra represents the current codec status of the A2DP
+ * profile.
+ */
+ public static final String EXTRA_CODEC_STATUS =
+ "android.bluetooth.extra.CODEC_STATUS";
+
+ private final @Nullable BluetoothCodecConfig mCodecConfig;
+ private final @Nullable List<BluetoothCodecConfig> mCodecsLocalCapabilities;
+ private final @Nullable List<BluetoothCodecConfig> mCodecsSelectableCapabilities;
+
+ /**
+ * Creates a new BluetoothCodecStatus.
+ *
+ * @hide
+ */
+ public BluetoothCodecStatus(@Nullable BluetoothCodecConfig codecConfig,
+ @Nullable List<BluetoothCodecConfig> codecsLocalCapabilities,
+ @Nullable List<BluetoothCodecConfig> codecsSelectableCapabilities) {
+ mCodecConfig = codecConfig;
+ mCodecsLocalCapabilities = codecsLocalCapabilities;
+ mCodecsSelectableCapabilities = codecsSelectableCapabilities;
+ }
+
+ private BluetoothCodecStatus(Parcel in) {
+ mCodecConfig = in.readTypedObject(BluetoothCodecConfig.CREATOR);
+ mCodecsLocalCapabilities = in.createTypedArrayList(BluetoothCodecConfig.CREATOR);
+ mCodecsSelectableCapabilities = in.createTypedArrayList(BluetoothCodecConfig.CREATOR);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothCodecStatus) {
+ BluetoothCodecStatus other = (BluetoothCodecStatus) o;
+ return (Objects.equals(other.mCodecConfig, mCodecConfig)
+ && sameCapabilities(other.mCodecsLocalCapabilities, mCodecsLocalCapabilities)
+ && sameCapabilities(other.mCodecsSelectableCapabilities,
+ mCodecsSelectableCapabilities));
+ }
+ return false;
+ }
+
+ /**
+ * Checks whether two lists of capabilities contain same capabilities.
+ * The order of the capabilities in each list is ignored.
+ *
+ * @param c1 the first list of capabilities to compare
+ * @param c2 the second list of capabilities to compare
+ * @return {@code true} if both lists contain same capabilities
+ */
+ private static boolean sameCapabilities(@Nullable List<BluetoothCodecConfig> c1,
+ @Nullable List<BluetoothCodecConfig> c2) {
+ if (c1 == null) {
+ return (c2 == null);
+ }
+ if (c2 == null) {
+ return false;
+ }
+ if (c1.size() != c2.size()) {
+ return false;
+ }
+ return c1.containsAll(c2);
+ }
+
+ /**
+ * Checks whether the codec config matches the selectable capabilities.
+ * Any parameters of the codec config with NONE value will be considered a wildcard matching.
+ *
+ * @param codecConfig the codec config to compare against
+ * @return {@code true} if the codec config matches, {@code false} otherwise
+ */
+ public boolean isCodecConfigSelectable(@Nullable BluetoothCodecConfig codecConfig) {
+ if (codecConfig == null || !codecConfig.hasSingleSampleRate()
+ || !codecConfig.hasSingleBitsPerSample() || !codecConfig.hasSingleChannelMode()) {
+ return false;
+ }
+ for (BluetoothCodecConfig selectableConfig : mCodecsSelectableCapabilities) {
+ if (codecConfig.getCodecType() != selectableConfig.getCodecType()) {
+ continue;
+ }
+ int sampleRate = codecConfig.getSampleRate();
+ if ((sampleRate & selectableConfig.getSampleRate()) == 0
+ && sampleRate != BluetoothCodecConfig.SAMPLE_RATE_NONE) {
+ continue;
+ }
+ int bitsPerSample = codecConfig.getBitsPerSample();
+ if ((bitsPerSample & selectableConfig.getBitsPerSample()) == 0
+ && bitsPerSample != BluetoothCodecConfig.BITS_PER_SAMPLE_NONE) {
+ continue;
+ }
+ int channelMode = codecConfig.getChannelMode();
+ if ((channelMode & selectableConfig.getChannelMode()) == 0
+ && channelMode != BluetoothCodecConfig.CHANNEL_MODE_NONE) {
+ continue;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a hash based on the codec config and local capabilities.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCodecConfig, mCodecsLocalCapabilities,
+ mCodecsLocalCapabilities);
+ }
+
+ /**
+ * Returns a {@link String} that describes each BluetoothCodecStatus parameter
+ * current value.
+ */
+ @Override
+ public String toString() {
+ return "{mCodecConfig:" + mCodecConfig
+ + ",mCodecsLocalCapabilities:" + mCodecsLocalCapabilities
+ + ",mCodecsSelectableCapabilities:" + mCodecsSelectableCapabilities
+ + "}";
+ }
+
+ /**
+ * @return 0
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<BluetoothCodecStatus> CREATOR = new Creator<>() {
+ public BluetoothCodecStatus createFromParcel(Parcel in) {
+ return new BluetoothCodecStatus(in);
+ }
+
+ public BluetoothCodecStatus[] newArray(int size) {
+ return new BluetoothCodecStatus[size];
+ }
+ };
+
+ /**
+ * Flattens the object to a parcel.
+ *
+ * @param out The Parcel in which the object should be written
+ * @param flags Additional flags about how the object should be written
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeTypedObject(mCodecConfig, 0);
+ out.writeTypedList(mCodecsLocalCapabilities);
+ out.writeTypedList(mCodecsSelectableCapabilities);
+ }
+
+ /**
+ * Returns the current codec configuration.
+ */
+ public @Nullable BluetoothCodecConfig getCodecConfig() {
+ return mCodecConfig;
+ }
+
+ /**
+ * Returns the codecs local capabilities.
+ */
+ public @NonNull List<BluetoothCodecConfig> getCodecsLocalCapabilities() {
+ return (mCodecsLocalCapabilities == null)
+ ? Collections.emptyList() : mCodecsLocalCapabilities;
+ }
+
+ /**
+ * Returns the codecs selectable capabilities.
+ */
+ public @NonNull List<BluetoothCodecConfig> getCodecsSelectableCapabilities() {
+ return (mCodecsSelectableCapabilities == null)
+ ? Collections.emptyList() : mCodecsSelectableCapabilities;
+ }
+
+ /**
+ * Builder for {@link BluetoothCodecStatus}.
+ */
+ public static final class Builder {
+ private BluetoothCodecConfig mCodecConfig = null;
+ private List<BluetoothCodecConfig> mCodecsLocalCapabilities = null;
+ private List<BluetoothCodecConfig> mCodecsSelectableCapabilities = null;
+
+ /**
+ * Set Bluetooth codec config for this codec status.
+ *
+ * @param codecConfig of this codec status
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecConfig(@NonNull BluetoothCodecConfig codecConfig) {
+ mCodecConfig = codecConfig;
+ return this;
+ }
+
+ /**
+ * Set codec local capabilities list for this codec status.
+ *
+ * @param codecsLocalCapabilities of this codec status
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecsLocalCapabilities(
+ @NonNull List<BluetoothCodecConfig> codecsLocalCapabilities) {
+ mCodecsLocalCapabilities = codecsLocalCapabilities;
+ return this;
+ }
+
+ /**
+ * Set codec selectable capabilities list for this codec status.
+ *
+ * @param codecsSelectableCapabilities of this codec status
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecsSelectableCapabilities(
+ @NonNull List<BluetoothCodecConfig> codecsSelectableCapabilities) {
+ mCodecsSelectableCapabilities = codecsSelectableCapabilities;
+ return this;
+ }
+
+ /**
+ * Build {@link BluetoothCodecStatus}.
+ * @return new BluetoothCodecStatus built
+ */
+ public @NonNull BluetoothCodecStatus build() {
+ return new BluetoothCodecStatus(mCodecConfig, mCodecsLocalCapabilities,
+ mCodecsSelectableCapabilities);
+ }
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothCsipSetCoordinator.java b/android-34/android/bluetooth/BluetoothCsipSetCoordinator.java
new file mode 100644
index 0000000..b9e3bf7
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothCsipSetCoordinator.java
@@ -0,0 +1,553 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Bluetooth CSIP set coordinator.
+ *
+ * <p>BluetoothCsipSetCoordinator is a proxy object for controlling the Bluetooth CSIP set
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothCsipSetCoordinator proxy object.
+ *
+ */
+public final class BluetoothCsipSetCoordinator implements BluetoothProfile, AutoCloseable {
+ private static final String TAG = "BluetoothCsipSetCoordinator";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private CloseGuard mCloseGuard;
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public interface ClientLockCallback {
+ /** @hide */
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED,
+ BluetoothStatusCodes.ERROR_CSIP_INVALID_GROUP_ID,
+ BluetoothStatusCodes.ERROR_CSIP_GROUP_LOCKED_BY_OTHER,
+ BluetoothStatusCodes.ERROR_CSIP_LOCKED_GROUP_MEMBER_LOST,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Status {}
+
+ /**
+ * Callback is invoken as a result on {@link #groupLock()}.
+ *
+ * @param groupId group identifier
+ * @param opStatus status of lock operation
+ * @param isLocked inidcates if group is locked
+ *
+ * @hide
+ */
+ @SystemApi
+ void onGroupLockSet(int groupId, @Status int opStatus, boolean isLocked);
+ }
+
+ private static class BluetoothCsipSetCoordinatorLockCallbackDelegate
+ extends IBluetoothCsipSetCoordinatorLockCallback.Stub {
+ private final ClientLockCallback mCallback;
+ private final Executor mExecutor;
+
+ BluetoothCsipSetCoordinatorLockCallbackDelegate(
+ Executor executor, ClientLockCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onGroupLockSet(int groupId, int opStatus, boolean isLocked) {
+ mExecutor.execute(() -> mCallback.onGroupLockSet(groupId, opStatus, isLocked));
+ }
+ };
+
+ /**
+ * Intent used to broadcast the change in connection state of the CSIS
+ * Client.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CSIS_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.action.CSIS_CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to expose broadcast receiving device.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote Broadcast receiver device. </li>
+ * <li> {@link #EXTRA_CSIS_GROUP_ID} - Group identifier. </li>
+ * <li> {@link #EXTRA_CSIS_GROUP_SIZE} - Group size. </li>
+ * <li> {@link #EXTRA_CSIS_GROUP_TYPE_UUID} - Group type UUID. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CSIS_DEVICE_AVAILABLE =
+ "android.bluetooth.action.CSIS_DEVICE_AVAILABLE";
+
+ /**
+ * Used as an extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
+ * Contains the group id.
+ *
+ * <p>Possible Values:
+ * {@link GROUP_ID_INVALID} Invalid group identifier
+ * 0x01 - 0xEF Valid group identifier
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_CSIS_GROUP_ID = "android.bluetooth.extra.CSIS_GROUP_ID";
+
+ /**
+ * Group size as int extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CSIS_GROUP_SIZE = "android.bluetooth.extra.CSIS_GROUP_SIZE";
+
+ /**
+ * Group type uuid extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CSIS_GROUP_TYPE_UUID =
+ "android.bluetooth.extra.CSIS_GROUP_TYPE_UUID";
+
+ /**
+ * Intent used to broadcast information about identified set member
+ * ready to connect.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * <li> {@link #EXTRA_CSIS_GROUP_ID} - Group identifier. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CSIS_SET_MEMBER_AVAILABLE =
+ "android.bluetooth.action.CSIS_SET_MEMBER_AVAILABLE";
+
+ /**
+ * This represents an invalid group ID.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int GROUP_ID_INVALID = IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothCsipSetCoordinator> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.CSIP_SET_COORDINATOR, TAG,
+ IBluetoothCsipSetCoordinator.class.getName()) {
+ @Override
+ public IBluetoothCsipSetCoordinator getServiceInterface(IBinder service) {
+ return IBluetoothCsipSetCoordinator.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothCsipSetCoordinator proxy object for interacting with the local
+ * Bluetooth CSIS service.
+ */
+ /*package*/ BluetoothCsipSetCoordinator(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * @hide
+ */
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothCsipSetCoordinator getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Lock the set.
+ * @param groupId group ID to lock,
+ * @param executor callback executor,
+ * @param callback callback to report lock and unlock events - stays valid until the app unlocks
+ * using the returned lock identifier or the lock timeouts on the remote side,
+ * as per CSIS specification,
+ * @return unique lock identifier used for unlocking or null if lock has failed.
+ * @throws {@link IllegalArgumentException} when executor or callback is null
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public
+ @Nullable UUID lockGroup(int groupId, @NonNull @CallbackExecutor Executor executor,
+ @NonNull ClientLockCallback callback) {
+ if (VDBG) log("lockGroup()");
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final UUID defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ IBluetoothCsipSetCoordinatorLockCallback delegate =
+ new BluetoothCsipSetCoordinatorLockCallbackDelegate(executor, callback);
+ try {
+ final SynchronousResultReceiver<ParcelUuid> recv = SynchronousResultReceiver.get();
+ service.lockGroup(groupId, delegate, mAttributionSource, recv);
+ final ParcelUuid ret = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ return ret == null ? defaultValue : ret.getUuid();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Unlock the set.
+ * @param lockUuid unique lock identifier
+ * @return true if unlocked, false on error
+ * @throws {@link IllegalArgumentException} when lockUuid is null
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean unlockGroup(@NonNull UUID lockUuid) {
+ if (VDBG) log("unlockGroup()");
+ Objects.requireNonNull(lockUuid, "lockUuid cannot be null");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ service.unlockGroup(new ParcelUuid(lockUuid), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ return true;
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get device's groups.
+ * @param device the active device
+ * @return Map of groups ids and related UUIDs
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @NonNull
+ public Map<Integer, ParcelUuid> getGroupUuidMapByDevice(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getGroupUuidMapByDevice()");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final Map defaultValue = new HashMap<>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Map> recv = SynchronousResultReceiver.get();
+ service.getGroupUuidMapByDevice(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get group id for the given UUID
+ * @param uuid
+ * @return list of group IDs
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public @NonNull List<Integer> getAllGroupIds(@Nullable ParcelUuid uuid) {
+ if (VDBG) log("getAllGroupIds()");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final List<Integer> defaultValue = new ArrayList<>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<Integer>> recv =
+ SynchronousResultReceiver.get();
+ service.getAllGroupIds(uuid, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @NonNull
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
+ if (VDBG) log("getDevicesMatchingStates(states=" + Arrays.toString(states) + ")");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @BluetoothProfile.BtProfileState
+ public int getConnectionState(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true if connectionPolicy is set, false on error
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean setConnectionPolicy(
+ @Nullable BluetoothDevice device, @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)
+ && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionPolicy(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(@Nullable BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothDevice.java b/android-34/android/bluetooth/BluetoothDevice.java
new file mode 100644
index 0000000..6965df1
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothDevice.java
@@ -0,0 +1,3629 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.app.compat.CompatChanges;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresBluetoothLocationPermission;
+import android.bluetooth.annotations.RequiresBluetoothScanPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.companion.AssociationRequest;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IpcDataCache;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Represents a remote Bluetooth device. A {@link BluetoothDevice} lets you
+ * create a connection with the respective device or query information about
+ * it, such as the name, address, class, and bonding state.
+ *
+ * <p>This class is really just a thin wrapper for a Bluetooth hardware
+ * address. Objects of this class are immutable. Operations on this class
+ * are performed on the remote Bluetooth hardware address, using the
+ * {@link BluetoothAdapter} that was used to create this {@link
+ * BluetoothDevice}.
+ *
+ * <p>To get a {@link BluetoothDevice}, use
+ * {@link BluetoothAdapter#getRemoteDevice(String)
+ * BluetoothAdapter.getRemoteDevice(String)} to create one representing a device
+ * of a known MAC address (which you can get through device discovery with
+ * {@link BluetoothAdapter}) or get one from the set of bonded devices
+ * returned by {@link BluetoothAdapter#getBondedDevices()
+ * BluetoothAdapter.getBondedDevices()}. You can then open a
+ * {@link BluetoothSocket} for communication with the remote device, using
+ * {@link #createRfcommSocketToServiceRecord(UUID)} over Bluetooth BR/EDR or using
+ * {@link #createL2capChannel(int)} over Bluetooth LE.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>
+ * For more information about using Bluetooth, read the <a href=
+ * "{@docRoot}guide/topics/connectivity/bluetooth.html">Bluetooth</a> developer
+ * guide.
+ * </p>
+ * </div>
+ *
+ * {@see BluetoothAdapter}
+ * {@see BluetoothSocket}
+ */
+public final class BluetoothDevice implements Parcelable, Attributable {
+ private static final String TAG = "BluetoothDevice";
+ private static final boolean DBG = false;
+
+ /**
+ * Connection state bitmask as returned by getConnectionState.
+ */
+ private static final int CONNECTION_STATE_DISCONNECTED = 0;
+ private static final int CONNECTION_STATE_CONNECTED = 1;
+ private static final int CONNECTION_STATE_ENCRYPTED_BREDR = 2;
+ private static final int CONNECTION_STATE_ENCRYPTED_LE = 4;
+
+ /**
+ * Sentinel error value for this class. Guaranteed to not equal any other
+ * integer constant in this class. Provided as a convenience for functions
+ * that require a sentinel error value, for example:
+ * <p><code>Intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ * BluetoothDevice.ERROR)</code>
+ */
+ public static final int ERROR = Integer.MIN_VALUE;
+
+ /**
+ * Broadcast Action: Remote device discovered.
+ * <p>Sent when a remote device is found during discovery.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_CLASS}. Can contain the extra fields {@link #EXTRA_NAME} and/or
+ * {@link #EXTRA_RSSI} and/or {@link #EXTRA_IS_COORDINATED_SET_MEMBER} if they are available.
+ */
+ // TODO: Change API to not broadcast RSSI if not available (incoming connection)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_FOUND =
+ "android.bluetooth.device.action.FOUND";
+
+ /**
+ * Broadcast Action: Bluetooth class of a remote device has changed.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_CLASS}.
+ * {@see BluetoothClass}
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CLASS_CHANGED =
+ "android.bluetooth.device.action.CLASS_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates a low level (ACL) connection has been
+ * established with a remote device.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link #EXTRA_TRANSPORT}.
+ * <p>ACL connections are managed automatically by the Android Bluetooth
+ * stack.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACL_CONNECTED =
+ "android.bluetooth.device.action.ACL_CONNECTED";
+
+ /**
+ * Broadcast Action: Indicates that a low level (ACL) disconnection has
+ * been requested for a remote device, and it will soon be disconnected.
+ * <p>This is useful for graceful disconnection. Applications should use
+ * this intent as a hint to immediately terminate higher level connections
+ * (RFCOMM, L2CAP, or profile connections) to the remote device.
+ * <p>Always contains the extra field {@link #EXTRA_DEVICE}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACL_DISCONNECT_REQUESTED =
+ "android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED";
+
+ /**
+ * Broadcast Action: Indicates a low level (ACL) disconnection from a
+ * remote device.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link #EXTRA_TRANSPORT}.
+ * <p>ACL connections are managed automatically by the Android Bluetooth
+ * stack.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACL_DISCONNECTED =
+ "android.bluetooth.device.action.ACL_DISCONNECTED";
+
+ /**
+ * Broadcast Action: Indicates the friendly name of a remote device has
+ * been retrieved for the first time, or changed since the last retrieval.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_NAME}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NAME_CHANGED =
+ "android.bluetooth.device.action.NAME_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates the alias of a remote device has been
+ * changed.
+ * <p>Always contains the extra field {@link #EXTRA_DEVICE}.
+ */
+ @SuppressLint("ActionValue")
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ALIAS_CHANGED =
+ "android.bluetooth.device.action.ALIAS_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates a change in the bond state of a remote
+ * device. For example, if a device is bonded (paired).
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE}, {@link
+ * #EXTRA_BOND_STATE} and {@link #EXTRA_PREVIOUS_BOND_STATE}.
+ */
+ // Note: When EXTRA_BOND_STATE is BOND_NONE then this will also
+ // contain a hidden extra field EXTRA_UNBOND_REASON with the result code.
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BOND_STATE_CHANGED =
+ "android.bluetooth.device.action.BOND_STATE_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates the battery level of a remote device has
+ * been retrieved for the first time, or changed since the last retrieval
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_BATTERY_LEVEL}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_BATTERY_LEVEL_CHANGED =
+ "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates the audio buffer size should be switched
+ * between a low latency buffer size and a higher and larger latency buffer size.
+ * Only registered receivers will receive this intent.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_LOW_LATENCY_BUFFER_SIZE}.
+ *
+ * @hide
+ */
+ @SuppressLint("ActionValue")
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_SWITCH_BUFFER_SIZE =
+ "android.bluetooth.device.action.SWITCH_BUFFER_SIZE";
+
+ /**
+ * Used as an Integer extra field in {@link #ACTION_BATTERY_LEVEL_CHANGED}
+ * intent. It contains the most recently retrieved battery level information
+ * ranging from 0% to 100% for a remote device, {@link #BATTERY_LEVEL_UNKNOWN}
+ * when the valid is unknown or there is an error, {@link #BATTERY_LEVEL_BLUETOOTH_OFF} when the
+ * bluetooth is off
+ *
+ * @hide
+ */
+ @SuppressLint("ActionValue")
+ @SystemApi
+ public static final String EXTRA_BATTERY_LEVEL =
+ "android.bluetooth.device.extra.BATTERY_LEVEL";
+
+ /**
+ * Used as the unknown value for {@link #EXTRA_BATTERY_LEVEL} and {@link #getBatteryLevel()}
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int BATTERY_LEVEL_UNKNOWN = -1;
+
+ /**
+ * Used as an error value for {@link #getBatteryLevel()} to represent bluetooth is off
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int BATTERY_LEVEL_BLUETOOTH_OFF = -100;
+
+ /**
+ * Used as a Parcelable {@link BluetoothDevice} extra field in every intent
+ * broadcast by this class. It contains the {@link BluetoothDevice} that
+ * the intent applies to.
+ */
+ public static final String EXTRA_DEVICE = "android.bluetooth.device.extra.DEVICE";
+
+ /**
+ * Used as a String extra field in {@link #ACTION_NAME_CHANGED} and {@link
+ * #ACTION_FOUND} intents. It contains the friendly Bluetooth name.
+ */
+ public static final String EXTRA_NAME = "android.bluetooth.device.extra.NAME";
+
+ /**
+ * Used as a Parcelable {@link BluetoothQualityReport} extra field in
+ * {@link #ACTION_REMOTE_ISSUE_OCCURRED} intent. It contains the {@link BluetoothQualityReport}.
+ * @hide
+ */
+ public static final String EXTRA_BQR = "android.bluetooth.qti.extra.EXTRA_BQR";
+
+ /**
+ * Used as an optional short extra field in {@link #ACTION_FOUND} intents.
+ * Contains the RSSI value of the remote device as reported by the
+ * Bluetooth hardware.
+ */
+ public static final String EXTRA_RSSI = "android.bluetooth.device.extra.RSSI";
+
+ /**
+ * Used as a boolean extra field in {@link #ACTION_FOUND} intents.
+ * It contains the information if device is discovered as member of a coordinated set or not.
+ * Pairing with device that belongs to a set would trigger pairing with the rest of set members.
+ * See Bluetooth CSIP specification for more details.
+ */
+ public static final String EXTRA_IS_COORDINATED_SET_MEMBER =
+ "android.bluetooth.extra.IS_COORDINATED_SET_MEMBER";
+
+ /**
+ * Used as a Parcelable {@link BluetoothClass} extra field in {@link
+ * #ACTION_FOUND} and {@link #ACTION_CLASS_CHANGED} intents.
+ */
+ public static final String EXTRA_CLASS = "android.bluetooth.device.extra.CLASS";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_BOND_STATE_CHANGED} intents.
+ * Contains the bond state of the remote device.
+ * <p>Possible values are:
+ * {@link #BOND_NONE},
+ * {@link #BOND_BONDING},
+ * {@link #BOND_BONDED}.
+ */
+ public static final String EXTRA_BOND_STATE = "android.bluetooth.device.extra.BOND_STATE";
+ /**
+ * Used as an int extra field in {@link #ACTION_BOND_STATE_CHANGED} intents.
+ * Contains the previous bond state of the remote device.
+ * <p>Possible values are:
+ * {@link #BOND_NONE},
+ * {@link #BOND_BONDING},
+ * {@link #BOND_BONDED}.
+ */
+ public static final String EXTRA_PREVIOUS_BOND_STATE =
+ "android.bluetooth.device.extra.PREVIOUS_BOND_STATE";
+
+ /**
+ * Used as a boolean extra field to indicate if audio buffer size is low latency or not
+ *
+ * @hide
+ */
+ @SuppressLint("ActionValue")
+ @SystemApi
+ public static final String EXTRA_LOW_LATENCY_BUFFER_SIZE =
+ "android.bluetooth.device.extra.LOW_LATENCY_BUFFER_SIZE";
+
+ /**
+ * Indicates the remote device is not bonded (paired).
+ * <p>There is no shared link key with the remote device, so communication
+ * (if it is allowed at all) will be unauthenticated and unencrypted.
+ */
+ public static final int BOND_NONE = 10;
+ /**
+ * Indicates bonding (pairing) is in progress with the remote device.
+ */
+ public static final int BOND_BONDING = 11;
+ /**
+ * Indicates the remote device is bonded (paired).
+ * <p>A shared link keys exists locally for the remote device, so
+ * communication can be authenticated and encrypted.
+ * <p><i>Being bonded (paired) with a remote device does not necessarily
+ * mean the device is currently connected. It just means that the pending
+ * procedure was completed at some earlier time, and the link key is still
+ * stored locally, ready to use on the next connection.
+ * </i>
+ */
+ public static final int BOND_BONDED = 12;
+
+ /**
+ * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST} intents for unbond reason.
+ * Possible value are :
+ * - {@link #UNBOND_REASON_AUTH_FAILED}
+ * - {@link #UNBOND_REASON_AUTH_REJECTED}
+ * - {@link #UNBOND_REASON_AUTH_CANCELED}
+ * - {@link #UNBOND_REASON_REMOTE_DEVICE_DOWN}
+ * - {@link #UNBOND_REASON_DISCOVERY_IN_PROGRESS}
+ * - {@link #UNBOND_REASON_AUTH_TIMEOUT}
+ * - {@link #UNBOND_REASON_REPEATED_ATTEMPTS}
+ * - {@link #UNBOND_REASON_REMOTE_AUTH_CANCELED}
+ * - {@link #UNBOND_REASON_REMOVED}
+ *
+ * Note: Can be added as a hidden extra field for {@link #ACTION_BOND_STATE_CHANGED} when the
+ * {@link #EXTRA_BOND_STATE} is {@link #BOND_NONE}
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_UNBOND_REASON = "android.bluetooth.device.extra.REASON";
+
+ /**
+ * Use {@link EXTRA_UNBOND_REASON} instead
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final String EXTRA_REASON = EXTRA_UNBOND_REASON;
+
+
+ /**
+ * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST}
+ * intents to indicate pairing method used. Possible values are:
+ * {@link #PAIRING_VARIANT_PIN},
+ * {@link #PAIRING_VARIANT_PASSKEY_CONFIRMATION},
+ */
+ public static final String EXTRA_PAIRING_VARIANT =
+ "android.bluetooth.device.extra.PAIRING_VARIANT";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST}
+ * intents as the value of passkey.
+ * The Bluetooth Passkey is a 6-digit numerical value represented as integer value
+ * in the range 0x00000000 – 0x000F423F (000000 to 999999).
+ */
+ public static final String EXTRA_PAIRING_KEY = "android.bluetooth.device.extra.PAIRING_KEY";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST}
+ * intents as the location of initiator. Possible value are:
+ * {@link #EXTRA_PAIRING_INITIATOR_FOREGROUND},
+ * {@link #EXTRA_PAIRING_INITIATOR_BACKGROUND},
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_PAIRING_INITIATOR =
+ "android.bluetooth.device.extra.PAIRING_INITIATOR";
+
+ /**
+ * Bluetooth pairing initiator, Foreground App
+ * @hide
+ */
+ @SystemApi
+ public static final int EXTRA_PAIRING_INITIATOR_FOREGROUND = 1;
+
+ /**
+ * Bluetooth pairing initiator, Background
+ * @hide
+ */
+ @SystemApi
+ public static final int EXTRA_PAIRING_INITIATOR_BACKGROUND = 2;
+
+ /**
+ * Bluetooth device type, Unknown
+ */
+ public static final int DEVICE_TYPE_UNKNOWN = 0;
+
+ /**
+ * Bluetooth device type, Classic - BR/EDR devices
+ */
+ public static final int DEVICE_TYPE_CLASSIC = 1;
+
+ /**
+ * Bluetooth device type, Low Energy - LE-only
+ */
+ public static final int DEVICE_TYPE_LE = 2;
+
+ /**
+ * Bluetooth device type, Dual Mode - BR/EDR/LE
+ */
+ public static final int DEVICE_TYPE_DUAL = 3;
+
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final String ACTION_SDP_RECORD =
+ "android.bluetooth.device.action.SDP_RECORD";
+
+ /** @hide */
+ @IntDef(prefix = "METADATA_", value = {
+ METADATA_MANUFACTURER_NAME,
+ METADATA_MODEL_NAME,
+ METADATA_SOFTWARE_VERSION,
+ METADATA_HARDWARE_VERSION,
+ METADATA_COMPANION_APP,
+ METADATA_MAIN_ICON,
+ METADATA_IS_UNTETHERED_HEADSET,
+ METADATA_UNTETHERED_LEFT_ICON,
+ METADATA_UNTETHERED_RIGHT_ICON,
+ METADATA_UNTETHERED_CASE_ICON,
+ METADATA_UNTETHERED_LEFT_BATTERY,
+ METADATA_UNTETHERED_RIGHT_BATTERY,
+ METADATA_UNTETHERED_CASE_BATTERY,
+ METADATA_UNTETHERED_LEFT_CHARGING,
+ METADATA_UNTETHERED_RIGHT_CHARGING,
+ METADATA_UNTETHERED_CASE_CHARGING,
+ METADATA_ENHANCED_SETTINGS_UI_URI,
+ METADATA_DEVICE_TYPE,
+ METADATA_MAIN_BATTERY,
+ METADATA_MAIN_CHARGING,
+ METADATA_MAIN_LOW_BATTERY_THRESHOLD,
+ METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD,
+ METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
+ METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD,
+ METADATA_SPATIAL_AUDIO,
+ METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ METADATA_LE_AUDIO,
+ METADATA_GMCS_CCCD,
+ METADATA_GTBS_CCCD})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MetadataKey{}
+
+ /**
+ * Maximum length of a metadata entry, this is to avoid exploding Bluetooth
+ * disk usage
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAX_LENGTH = 2048;
+
+ /**
+ * Manufacturer name of this Bluetooth device
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MANUFACTURER_NAME = 0;
+
+ /**
+ * Model name of this Bluetooth device
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MODEL_NAME = 1;
+
+ /**
+ * Software version of this Bluetooth device
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_SOFTWARE_VERSION = 2;
+
+ /**
+ * Hardware version of this Bluetooth device
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_HARDWARE_VERSION = 3;
+
+ /**
+ * Package name of the companion app, if any
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_COMPANION_APP = 4;
+
+ /**
+ * URI to the main icon shown on the settings UI
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_ICON = 5;
+
+ /**
+ * Whether this device is an untethered headset with left, right and case
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_IS_UNTETHERED_HEADSET = 6;
+
+ /**
+ * URI to icon of the left headset
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_LEFT_ICON = 7;
+
+ /**
+ * URI to icon of the right headset
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_RIGHT_ICON = 8;
+
+ /**
+ * URI to icon of the headset charging case
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_CASE_ICON = 9;
+
+ /**
+ * Battery level of left headset
+ * Data type should be {@String} 0-100 as {@link Byte} array, otherwise
+ * as invalid.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_LEFT_BATTERY = 10;
+
+ /**
+ * Battery level of rigth headset
+ * Data type should be {@String} 0-100 as {@link Byte} array, otherwise
+ * as invalid.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_RIGHT_BATTERY = 11;
+
+ /**
+ * Battery level of the headset charging case
+ * Data type should be {@String} 0-100 as {@link Byte} array, otherwise
+ * as invalid.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_CASE_BATTERY = 12;
+
+ /**
+ * Whether the left headset is charging
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_LEFT_CHARGING = 13;
+
+ /**
+ * Whether the right headset is charging
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_RIGHT_CHARGING = 14;
+
+ /**
+ * Whether the headset charging case is charging
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_CASE_CHARGING = 15;
+
+ /**
+ * URI to the enhanced settings UI slice
+ * Data type should be {@String} as {@link Byte} array, null means
+ * the UI does not exist.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_ENHANCED_SETTINGS_UI_URI = 16;
+
+ /**
+ * @hide
+ */
+ public static final String COMPANION_TYPE_PRIMARY = "COMPANION_PRIMARY";
+
+ /**
+ * @hide
+ */
+ public static final String COMPANION_TYPE_SECONDARY = "COMPANION_SECONDARY";
+
+ /**
+ * @hide
+ */
+ public static final String COMPANION_TYPE_NONE = "COMPANION_NONE";
+
+ /**
+ * Type of the Bluetooth device, must be within the list of
+ * BluetoothDevice.DEVICE_TYPE_*
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_DEVICE_TYPE = 17;
+
+ /**
+ * Battery level of the Bluetooth device, use when the Bluetooth device
+ * does not support HFP battery indicator.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_BATTERY = 18;
+
+ /**
+ * Whether the device is charging.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_CHARGING = 19;
+
+ /**
+ * The battery threshold of the Bluetooth device to show low battery icon.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_LOW_BATTERY_THRESHOLD = 20;
+
+ /**
+ * The battery threshold of the left headset to show low battery icon.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD = 21;
+
+ /**
+ * The battery threshold of the right headset to show low battery icon.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD = 22;
+
+ /**
+ * The battery threshold of the case to show low battery icon.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD = 23;
+
+
+ /**
+ * The metadata of the audio spatial data.
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ public static final int METADATA_SPATIAL_AUDIO = 24;
+
+ /**
+ * The metadata of the Fast Pair for any custmized feature.
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ public static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
+
+ /**
+ * The metadata of the Fast Pair for LE Audio capable devices.
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_LE_AUDIO = 26;
+
+ /**
+ * The UUIDs (16-bit) of registered to CCC characteristics from Media Control services.
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ public static final int METADATA_GMCS_CCCD = 27;
+
+ /**
+ * The UUIDs (16-bit) of registered to CCC characteristics from Telephony Bearer service.
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ public static final int METADATA_GTBS_CCCD = 28;
+
+ private static final int METADATA_MAX_KEY = METADATA_GTBS_CCCD;
+
+ /**
+ * Device type which is used in METADATA_DEVICE_TYPE
+ * Indicates this Bluetooth device is a standard Bluetooth accessory or
+ * not listed in METADATA_DEVICE_TYPE_*.
+ * @hide
+ */
+ @SystemApi
+ public static final String DEVICE_TYPE_DEFAULT = "Default";
+
+ /**
+ * Device type which is used in METADATA_DEVICE_TYPE
+ * Indicates this Bluetooth device is a watch.
+ * @hide
+ */
+ @SystemApi
+ public static final String DEVICE_TYPE_WATCH = "Watch";
+
+ /**
+ * Device type which is used in METADATA_DEVICE_TYPE
+ * Indicates this Bluetooth device is an untethered headset.
+ * @hide
+ */
+ @SystemApi
+ public static final String DEVICE_TYPE_UNTETHERED_HEADSET = "Untethered Headset";
+
+ /**
+ * Device type which is used in METADATA_DEVICE_TYPE
+ * Indicates this Bluetooth device is a stylus.
+ * @hide
+ */
+ @SystemApi
+ public static final String DEVICE_TYPE_STYLUS = "Stylus";
+
+ /**
+ * Broadcast Action: This intent is used to broadcast the {@link UUID}
+ * wrapped as a {@link android.os.ParcelUuid} of the remote device after it
+ * has been fetched. This intent is sent only when the UUIDs of the remote
+ * device are requested to be fetched using Service Discovery Protocol
+ * <p> Always contains the extra field {@link #EXTRA_DEVICE}
+ * <p> Always contains the extra field {@link #EXTRA_UUID}
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_UUID =
+ "android.bluetooth.device.action.UUID";
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MAS_INSTANCE =
+ "android.bluetooth.device.action.MAS_INSTANCE";
+
+ /**
+ * Broadcast Action: Indicates a failure to retrieve the name of a remote
+ * device.
+ * <p>Always contains the extra field {@link #EXTRA_DEVICE}.
+ *
+ * @hide
+ */
+ //TODO: is this actually useful?
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NAME_FAILED =
+ "android.bluetooth.device.action.NAME_FAILED";
+
+ /**
+ * Broadcast Action: This intent is used to broadcast PAIRING REQUEST
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PAIRING_REQUEST =
+ "android.bluetooth.device.action.PAIRING_REQUEST";
+
+ /**
+ * Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * the return value of {@link BluetoothDevice#toString()} has changed
+ * to improve privacy.
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private static final long CHANGE_TO_STRING_REDACTED = 265103382L;
+
+ /**
+ * Broadcast Action: This intent is used to broadcast PAIRING CANCEL
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_PAIRING_CANCEL =
+ "android.bluetooth.device.action.PAIRING_CANCEL";
+
+ /**
+ * Broadcast Action: This intent is used to broadcast CONNECTION ACCESS REQUEST
+ *
+ * This action will trigger a prompt for the user to accept or deny giving the
+ * permission for this device. Permissions can be specified with
+ * {@link #EXTRA_ACCESS_REQUEST_TYPE}.
+ *
+ * The reply will be an {@link #ACTION_CONNECTION_ACCESS_REPLY} sent to the specified
+ * {@link #EXTRA_PACKAGE_NAME} and {@link #EXTRA_CLASS_NAME}.
+ *
+ * This action can be cancelled with {@link #ACTION_CONNECTION_ACCESS_CANCEL}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_CONNECTION_ACCESS_REQUEST =
+ "android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST";
+
+ /**
+ * Broadcast Action: This intent is used to broadcast CONNECTION ACCESS REPLY
+ *
+ * This action is the reply from {@link #ACTION_CONNECTION_ACCESS_REQUEST}
+ * that is sent to the specified {@link #EXTRA_PACKAGE_NAME}
+ * and {@link #EXTRA_CLASS_NAME}.
+ *
+ * See the extra fields {@link #EXTRA_CONNECTION_ACCESS_RESULT} and
+ * {@link #EXTRA_ALWAYS_ALLOWED} for possible results.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_CONNECTION_ACCESS_REPLY =
+ "android.bluetooth.device.action.CONNECTION_ACCESS_REPLY";
+
+ /**
+ * Broadcast Action: This intent is used to broadcast CONNECTION ACCESS CANCEL
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_CONNECTION_ACCESS_CANCEL =
+ "android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL";
+
+ /**
+ * Intent to broadcast silence mode changed.
+ * Alway contains the extra field {@link #EXTRA_DEVICE}
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_SILENCE_MODE_CHANGED =
+ "android.bluetooth.device.action.SILENCE_MODE_CHANGED";
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST}.
+ *
+ * Possible values are {@link #REQUEST_TYPE_PROFILE_CONNECTION},
+ * {@link #REQUEST_TYPE_PHONEBOOK_ACCESS}, {@link #REQUEST_TYPE_MESSAGE_ACCESS}
+ * and {@link #REQUEST_TYPE_SIM_ACCESS}
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_ACCESS_REQUEST_TYPE =
+ "android.bluetooth.device.extra.ACCESS_REQUEST_TYPE";
+
+ /** @hide */
+ @SystemApi
+ public static final int REQUEST_TYPE_PROFILE_CONNECTION = 1;
+
+ /** @hide */
+ @SystemApi
+ public static final int REQUEST_TYPE_PHONEBOOK_ACCESS = 2;
+
+ /** @hide */
+ @SystemApi
+ public static final int REQUEST_TYPE_MESSAGE_ACCESS = 3;
+
+ /** @hide */
+ @SystemApi
+ public static final int REQUEST_TYPE_SIM_ACCESS = 4;
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intents,
+ * Contains package name to return reply intent to.
+ *
+ * @hide
+ */
+ public static final String EXTRA_PACKAGE_NAME = "android.bluetooth.device.extra.PACKAGE_NAME";
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intents,
+ * Contains class name to return reply intent to.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CLASS_NAME = "android.bluetooth.device.extra.CLASS_NAME";
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REPLY} intent.
+ *
+ * Possible values are {@link #CONNECTION_ACCESS_YES} and {@link #CONNECTION_ACCESS_NO}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_CONNECTION_ACCESS_RESULT =
+ "android.bluetooth.device.extra.CONNECTION_ACCESS_RESULT";
+
+ /** @hide */
+ @SystemApi
+ public static final int CONNECTION_ACCESS_YES = 1;
+
+ /** @hide */
+ @SystemApi
+ public static final int CONNECTION_ACCESS_NO = 2;
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REPLY} intents,
+ * Contains boolean to indicate if the allowed response is once-for-all so that
+ * next request will be granted without asking user again.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_ALWAYS_ALLOWED =
+ "android.bluetooth.device.extra.ALWAYS_ALLOWED";
+
+ /**
+ * A bond attempt succeeded
+ *
+ * @hide
+ */
+ public static final int BOND_SUCCESS = 0;
+
+ /**
+ * A bond attempt failed because pins did not match, or remote device did
+ * not respond to pin request in time
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_AUTH_FAILED = 1;
+
+ /**
+ * A bond attempt failed because the other side explicitly rejected
+ * bonding
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_AUTH_REJECTED = 2;
+
+ /**
+ * A bond attempt failed because we canceled the bonding process
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_AUTH_CANCELED = 3;
+
+ /**
+ * A bond attempt failed because we could not contact the remote device
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_REMOTE_DEVICE_DOWN = 4;
+
+ /**
+ * A bond attempt failed because a discovery is in progress
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_DISCOVERY_IN_PROGRESS = 5;
+
+ /**
+ * A bond attempt failed because of authentication timeout
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_AUTH_TIMEOUT = 6;
+
+ /**
+ * A bond attempt failed because of repeated attempts
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_REPEATED_ATTEMPTS = 7;
+
+ /**
+ * A bond attempt failed because we received an Authentication Cancel
+ * by remote end
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_REMOTE_AUTH_CANCELED = 8;
+
+ /**
+ * An existing bond was explicitly revoked
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UNBOND_REASON_REMOVED = 9;
+
+ /**
+ * The user will be prompted to enter a pin or
+ * an app will enter a pin for user.
+ */
+ public static final int PAIRING_VARIANT_PIN = 0;
+
+ /**
+ * The user will be prompted to enter a passkey
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int PAIRING_VARIANT_PASSKEY = 1;
+
+ /**
+ * The user will be prompted to confirm the passkey displayed on the screen or
+ * an app will confirm the passkey for the user.
+ */
+ public static final int PAIRING_VARIANT_PASSKEY_CONFIRMATION = 2;
+
+ /**
+ * The user will be prompted to accept or deny the incoming pairing request
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int PAIRING_VARIANT_CONSENT = 3;
+
+ /**
+ * The user will be prompted to enter the passkey displayed on remote device
+ * This is used for Bluetooth 2.1 pairing.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
+
+ /**
+ * The user will be prompted to enter the PIN displayed on remote device.
+ * This is used for Bluetooth 2.0 pairing.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int PAIRING_VARIANT_DISPLAY_PIN = 5;
+
+ /**
+ * The user will be prompted to accept or deny the OOB pairing request.
+ * This is used for Bluetooth 2.1 secure simple pairing.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int PAIRING_VARIANT_OOB_CONSENT = 6;
+
+ /**
+ * The user will be prompted to enter a 16 digit pin or
+ * an app will enter a 16 digit pin for user.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int PAIRING_VARIANT_PIN_16_DIGITS = 7;
+
+ /**
+ * Used as an extra field in {@link #ACTION_UUID} intents,
+ * Contains the {@link android.os.ParcelUuid}s of the remote device which
+ * is a parcelable version of {@link UUID}.
+ * A {@code null} EXTRA_UUID indicates a timeout.
+ */
+ public static final String EXTRA_UUID = "android.bluetooth.device.extra.UUID";
+
+ /** @hide */
+ public static final String EXTRA_SDP_RECORD =
+ "android.bluetooth.device.extra.SDP_RECORD";
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final String EXTRA_SDP_SEARCH_STATUS =
+ "android.bluetooth.device.extra.SDP_SEARCH_STATUS";
+
+ /** @hide */
+ @IntDef(prefix = "ACCESS_", value = {ACCESS_UNKNOWN,
+ ACCESS_ALLOWED, ACCESS_REJECTED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AccessPermission{}
+
+ /**
+ * For {@link #getPhonebookAccessPermission}, {@link #setPhonebookAccessPermission},
+ * {@link #getMessageAccessPermission} and {@link #setMessageAccessPermission}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACCESS_UNKNOWN = 0;
+
+ /**
+ * For {@link #getPhonebookAccessPermission}, {@link #setPhonebookAccessPermission},
+ * {@link #getMessageAccessPermission} and {@link #setMessageAccessPermission}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACCESS_ALLOWED = 1;
+
+ /**
+ * For {@link #getPhonebookAccessPermission}, {@link #setPhonebookAccessPermission},
+ * {@link #getMessageAccessPermission} and {@link #setMessageAccessPermission}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACCESS_REJECTED = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = { "TRANSPORT_" },
+ value = {
+ TRANSPORT_AUTO,
+ TRANSPORT_BREDR,
+ TRANSPORT_LE,
+ }
+ )
+ public @interface Transport {}
+
+ /**
+ * No preference of physical transport for GATT connections to remote dual-mode devices
+ */
+ public static final int TRANSPORT_AUTO = 0;
+
+ /**
+ * Constant representing the BR/EDR transport.
+ */
+ public static final int TRANSPORT_BREDR = 1;
+
+ /**
+ * Constant representing the Bluetooth Low Energy (BLE) Transport.
+ */
+ public static final int TRANSPORT_LE = 2;
+
+ /**
+ * Bluetooth LE 1M PHY. Used to refer to LE 1M Physical Channel for advertising, scanning or
+ * connection.
+ */
+ public static final int PHY_LE_1M = 1;
+
+ /**
+ * Bluetooth LE 2M PHY. Used to refer to LE 2M Physical Channel for advertising, scanning or
+ * connection.
+ */
+ public static final int PHY_LE_2M = 2;
+
+ /**
+ * Bluetooth LE Coded PHY. Used to refer to LE Coded Physical Channel for advertising, scanning
+ * or connection.
+ */
+ public static final int PHY_LE_CODED = 3;
+
+ /**
+ * Bluetooth LE 1M PHY mask. Used to specify LE 1M Physical Channel as one of many available
+ * options in a bitmask.
+ */
+ public static final int PHY_LE_1M_MASK = 1;
+
+ /**
+ * Bluetooth LE 2M PHY mask. Used to specify LE 2M Physical Channel as one of many available
+ * options in a bitmask.
+ */
+ public static final int PHY_LE_2M_MASK = 2;
+
+ /**
+ * Bluetooth LE Coded PHY mask. Used to specify LE Coded Physical Channel as one of many
+ * available options in a bitmask.
+ */
+ public static final int PHY_LE_CODED_MASK = 4;
+
+ /**
+ * No preferred coding when transmitting on the LE Coded PHY.
+ */
+ public static final int PHY_OPTION_NO_PREFERRED = 0;
+
+ /**
+ * Prefer the S=2 coding to be used when transmitting on the LE Coded PHY.
+ */
+ public static final int PHY_OPTION_S2 = 1;
+
+ /**
+ * Prefer the S=8 coding to be used when transmitting on the LE Coded PHY.
+ */
+ public static final int PHY_OPTION_S8 = 2;
+
+
+ /** @hide */
+ public static final String EXTRA_MAS_INSTANCE =
+ "android.bluetooth.device.extra.MAS_INSTANCE";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_ACL_CONNECTED} and
+ * {@link #ACTION_ACL_DISCONNECTED} intents to indicate which transport is connected.
+ * Possible values are: {@link #TRANSPORT_BREDR} and {@link #TRANSPORT_LE}.
+ */
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_TRANSPORT = "android.bluetooth.device.extra.TRANSPORT";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = { "ADDRESS_TYPE_" },
+ value = {
+ ADDRESS_TYPE_PUBLIC,
+ ADDRESS_TYPE_RANDOM,
+ ADDRESS_TYPE_UNKNOWN,
+ }
+ )
+ public @interface AddressType {}
+
+ /** Hardware MAC Address of the device */
+ public static final int ADDRESS_TYPE_PUBLIC = 0;
+ /** Address is either resolvable, non-resolvable or static. */
+ public static final int ADDRESS_TYPE_RANDOM = 1;
+ /** Address type is unknown or unavailable **/
+ public static final int ADDRESS_TYPE_UNKNOWN = 0xFFFF;
+
+ private static final String NULL_MAC_ADDRESS = "00:00:00:00:00:00";
+
+ private final String mAddress;
+ @AddressType private final int mAddressType;
+
+ private static boolean sIsLogRedactionFlagSynced = false;
+ private static boolean sIsLogRedactionEnabled = true;
+
+ private AttributionSource mAttributionSource;
+
+ static IBluetooth getService() {
+ return BluetoothAdapter.getDefaultAdapter().getBluetoothService();
+ }
+
+ /**
+ * Create a new BluetoothDevice.
+ * Bluetooth MAC address must be upper case, such as "00:11:22:33:AA:BB",
+ * and is validated in this constructor.
+ *
+ * @param address valid Bluetooth MAC address
+ * @param addressType valid address type
+ * @throws RuntimeException Bluetooth is not available on this platform
+ * @throws IllegalArgumentException address or addressType is invalid
+ * @hide
+ */
+ /*package*/ BluetoothDevice(String address, int addressType) {
+ if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+ throw new IllegalArgumentException(address + " is not a valid Bluetooth address");
+ }
+
+ if (addressType != ADDRESS_TYPE_PUBLIC && addressType != ADDRESS_TYPE_RANDOM) {
+ throw new IllegalArgumentException(addressType + " is not a Bluetooth address type");
+ }
+
+ mAddress = address;
+ mAddressType = addressType;
+ mAttributionSource = AttributionSource.myAttributionSource();
+ }
+
+ /**
+ * Create a new BluetoothDevice.
+ * Bluetooth MAC address must be upper case, such as "00:11:22:33:AA:BB",
+ * and is validated in this constructor.
+ *
+ * @param address valid Bluetooth MAC address
+ * @throws RuntimeException Bluetooth is not available on this platform
+ * @throws IllegalArgumentException address is invalid
+ * @hide
+ */
+ @UnsupportedAppUsage
+ /*package*/ BluetoothDevice(String address) {
+ this(address, ADDRESS_TYPE_PUBLIC);
+ }
+
+ /**
+ * Create a new BluetoothDevice.
+ *
+ * @param in valid parcel
+ * @throws RuntimeException Bluetooth is not available on this platform
+ * @throws IllegalArgumentException address is invalid
+ * @hide
+ */
+ @UnsupportedAppUsage
+ /*package*/ BluetoothDevice(Parcel in) {
+ this(in.readString(), in.readInt());
+ }
+
+ /** {@hide} */
+ public void setAttributionSource(@NonNull AttributionSource attributionSource) {
+ mAttributionSource = attributionSource;
+ }
+
+ /**
+ * Method should never be used anywhere. Only exception is from {@link Intent}
+ * Used to set the device current attribution source
+ *
+ * @param attributionSource The associated {@link AttributionSource} for this device in this
+ * process
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void prepareToEnterProcess(@NonNull AttributionSource attributionSource) {
+ setAttributionSource(attributionSource);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothDevice) {
+ return mAddress.equals(((BluetoothDevice) o).getAddress());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mAddress.hashCode();
+ }
+
+ /**
+ * Returns a string representation of this BluetoothDevice.
+ * <p> For apps targeting {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}
+ * (API level 34) or higher, this returns the MAC address of the device redacted
+ * by replacing the hexadecimal digits of leftmost 4 bytes (in big endian order)
+ * with "XX", e.g., "XX:XX:XX:XX:12:34". For apps targeting earlier versions,
+ * the MAC address is returned without redaction.
+ *
+ * Warning: The return value of {@link #toString()} may change in the future.
+ * It is intended to be used in logging statements. Thus apps should never rely
+ * on the return value of {@link #toString()} in their logic. Always use other
+ * appropriate APIs instead (e.g., use {@link #getAddress()} to get the MAC address).
+ *
+ * @return string representation of this BluetoothDevice
+ */
+ @Override
+ public String toString() {
+ if (!CompatChanges.isChangeEnabled(CHANGE_TO_STRING_REDACTED)) {
+ return mAddress;
+ }
+ return toStringForLogging();
+ }
+
+ private static boolean shouldLogBeRedacted() {
+ boolean defaultValue = true;
+ if (!sIsLogRedactionFlagSynced) {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null || !adapter.isEnabled()) {
+ return defaultValue;
+ }
+ IBluetooth service = adapter.getBluetoothService();
+
+ if (service == null) {
+ Log.e(TAG, "Bluetooth service is not enabled");
+ return defaultValue;
+ }
+
+ try {
+ sIsLogRedactionEnabled = service.isLogRedactionEnabled();
+ sIsLogRedactionFlagSynced = true;
+ } catch (RemoteException e) {
+ // by default, set to true
+ Log.e(TAG, "Failed to call IBluetooth.isLogRedactionEnabled"
+ + e.toString() + "\n"
+ + Log.getStackTraceString(new Throwable()));
+ return true;
+ }
+ }
+ return sIsLogRedactionEnabled;
+ }
+
+ /**
+ * Returns a string representation of this BluetoothDevice for logging.
+ * So far, this function only returns hardware address.
+ * If more information is needed, add it here
+ *
+ * @return string representation of this BluetoothDevice used for logging
+ * @hide
+ */
+ public String toStringForLogging() {
+ return getAddressForLogging();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<BluetoothDevice> CREATOR = new Creator<>() {
+ public BluetoothDevice createFromParcel(Parcel in) {
+ return new BluetoothDevice(in);
+ }
+
+ public BluetoothDevice[] newArray(int size) {
+ return new BluetoothDevice[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mAddress);
+ out.writeInt(mAddressType);
+ }
+
+ /**
+ * Returns the hardware address of this BluetoothDevice.
+ * <p> For example, "00:11:22:AA:BB:CC".
+ *
+ * @return Bluetooth hardware address as string
+ */
+ public String getAddress() {
+ if (DBG) Log.d(TAG, "getAddress: mAddress=" + getAddressForLogging());
+ return mAddress;
+ }
+
+ /**
+ * Returns the address type of this BluetoothDevice.
+ *
+ * @return Bluetooth address type
+ * @hide
+ */
+ public int getAddressType() {
+ if (DBG) Log.d(TAG, "mAddressType: " + mAddressType);
+ return mAddressType;
+ }
+
+ /**
+ * Returns the anonymized hardware address of this BluetoothDevice. The first three octets
+ * will be suppressed for anonymization.
+ * <p> For example, "XX:XX:XX:AA:BB:CC".
+ *
+ * @return Anonymized bluetooth hardware address as string
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public String getAnonymizedAddress() {
+ return BluetoothUtils.toAnonymizedAddress(mAddress);
+ }
+
+ /**
+ * Returns string representation of the hardware address of this BluetoothDevice
+ * for logging purpose. Depending on the build type and device config,
+ * this function returns either full address string (returned by getAddress),
+ * or a redacted string with the leftmost 4 bytes shown as 'xx',
+ * <p> For example, "xx:xx:xx:xx:aa:bb".
+ * This function is intended to avoid leaking full address in logs.
+ *
+ * @return string representation of the hardware address for logging
+ * @hide
+ */
+ public String getAddressForLogging() {
+ if (shouldLogBeRedacted()) {
+ return getAnonymizedAddress();
+ }
+ return mAddress;
+ }
+
+ /**
+ * Returns the identity address of this BluetoothDevice.
+ * <p> For example, "00:11:22:AA:BB:CC".
+ *
+ * @return Bluetooth identity address as a string
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Nullable String getIdentityAddress() {
+ if (DBG) log("getIdentityAddress()");
+ final IBluetooth service = getService();
+ final String defaultValue = null;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot get identity address");
+ } else {
+ try {
+ final SynchronousResultReceiver<String> recv = SynchronousResultReceiver.get();
+ service.getIdentityAddress(mAddress, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the friendly Bluetooth name of the remote device.
+ *
+ * <p>The local adapter will automatically retrieve remote names when
+ * performing a device scan, and will cache them. This method just returns
+ * the name for this device from the cache.
+ *
+ * @return the Bluetooth name, or null if there was a problem.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public String getName() {
+ if (DBG) log("getName()");
+ final IBluetooth service = getService();
+ final String defaultValue = null;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot get Remote Device name");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<String> recv = SynchronousResultReceiver.get();
+ service.getRemoteName(this, mAttributionSource, recv);
+ String name = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ if (name != null) {
+ // remove whitespace characters from the name
+ return name
+ .replace('\t', ' ')
+ .replace('\n', ' ')
+ .replace('\r', ' ');
+ }
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the Bluetooth device type of the remote device.
+ *
+ * @return the device type {@link #DEVICE_TYPE_CLASSIC}, {@link #DEVICE_TYPE_LE} {@link
+ * #DEVICE_TYPE_DUAL}. {@link #DEVICE_TYPE_UNKNOWN} if it's not available
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getType() {
+ if (DBG) log("getType()");
+ final IBluetooth service = getService();
+ final int defaultValue = DEVICE_TYPE_UNKNOWN;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot get Remote Device type");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getRemoteType(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the locally modifiable name (alias) of the remote Bluetooth device.
+ *
+ * @return the Bluetooth alias, the friendly device name if no alias, or
+ * null if there was a problem
+ */
+ @Nullable
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public String getAlias() {
+ if (DBG) log("getAlias()");
+ final IBluetooth service = getService();
+ final String defaultValue = null;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot get Remote Device Alias");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<String> recv = SynchronousResultReceiver.get();
+ service.getRemoteAlias(this, mAttributionSource, recv);
+ String alias = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ if (alias == null) {
+ return getName();
+ }
+ return alias
+ .replace('\t', ' ')
+ .replace('\n', ' ')
+ .replace('\r', ' ');
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED
+ })
+ public @interface SetAliasReturnValues{}
+
+ /**
+ * Sets the locally modifiable name (alias) of the remote Bluetooth device. This method
+ * overwrites the previously stored alias. The new alias is saved in local
+ * storage so that the change is preserved over power cycles.
+ *
+ * <p>This method requires the calling app to be associated with Companion Device Manager (see
+ * {@link android.companion.CompanionDeviceManager#associate(AssociationRequest,
+ * android.companion.CompanionDeviceManager.Callback, Handler)}) and have the
+ * {@link android.Manifest.permission#BLUETOOTH_CONNECT} permission. Alternatively, if the
+ * caller has the {@link android.Manifest.permission#BLUETOOTH_PRIVILEGED} permission, they can
+ * bypass the Companion Device Manager association requirement as well as other permission
+ * requirements.
+ *
+ * @param alias is the new locally modifiable name for the remote Bluetooth device which must
+ * be the empty string. If null, we clear the alias.
+ * @return whether the alias was successfully changed
+ * @throws IllegalArgumentException if the alias is the empty string
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @SetAliasReturnValues int setAlias(@Nullable String alias) {
+ if (alias != null && alias.isEmpty()) {
+ throw new IllegalArgumentException("alias cannot be the empty string");
+ }
+ if (DBG) log("setAlias(" + alias + ")");
+ final IBluetooth service = getService();
+ final int defaultValue = BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot set Remote Device name");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.setRemoteAlias(this, alias, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the most recent identified battery level of this Bluetooth device
+ *
+ * @return Battery level in percents from 0 to 100, {@link #BATTERY_LEVEL_BLUETOOTH_OFF} if
+ * Bluetooth is disabled or {@link #BATTERY_LEVEL_UNKNOWN} if device is disconnected, or does
+ * not have any battery reporting service, or return value is invalid
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @IntRange(from = -100, to = 100) int getBatteryLevel() {
+ if (DBG) log("getBatteryLevel()");
+ final IBluetooth service = getService();
+ final int defaultValue = BATTERY_LEVEL_BLUETOOTH_OFF;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth disabled. Cannot get remote device battery level");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getBatteryLevel(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Start the bonding (pairing) process with the remote device.
+ * <p>This is an asynchronous call, it will return immediately. Register
+ * for {@link #ACTION_BOND_STATE_CHANGED} intents to be notified when
+ * the bonding process completes, and its result.
+ * <p>Android system services will handle the necessary user interactions
+ * to confirm and complete the bonding process.
+ *
+ * @return false on immediate error, true if bonding will begin
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean createBond() {
+ return createBond(TRANSPORT_AUTO);
+ }
+
+ /**
+ * Start the bonding (pairing) process with the remote device using the
+ * specified transport.
+ *
+ * <p>This is an asynchronous call, it will return immediately. Register
+ * for {@link #ACTION_BOND_STATE_CHANGED} intents to be notified when
+ * the bonding process completes, and its result.
+ * <p>Android system services will handle the necessary user interactions
+ * to confirm and complete the bonding process.
+ *
+ * @param transport The transport to use for the pairing procedure.
+ * @return false on immediate error, true if bonding will begin
+ * @throws IllegalArgumentException if an invalid transport was specified
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean createBond(int transport) {
+ return createBondInternal(transport, null, null);
+ }
+
+ /**
+ * Start the bonding (pairing) process with the remote device using the
+ * Out Of Band mechanism.
+ *
+ * <p>This is an asynchronous call, it will return immediately. Register
+ * for {@link #ACTION_BOND_STATE_CHANGED} intents to be notified when
+ * the bonding process completes, and its result.
+ *
+ * <p>Android system services will handle the necessary user interactions
+ * to confirm and complete the bonding process.
+ *
+ * <p>There are two possible versions of OOB Data. This data can come in as
+ * P192 or P256. This is a reference to the cryptography used to generate the key.
+ * The caller may pass one or both. If both types of data are passed, then the
+ * P256 data will be preferred, and thus used.
+ *
+ * @param transport - Transport to use
+ * @param remoteP192Data - Out Of Band data (P192) or null
+ * @param remoteP256Data - Out Of Band data (P256) or null
+ * @return false on immediate error, true if bonding will begin
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean createBondOutOfBand(int transport, @Nullable OobData remoteP192Data,
+ @Nullable OobData remoteP256Data) {
+ if (remoteP192Data == null && remoteP256Data == null) {
+ throw new IllegalArgumentException(
+ "One or both arguments for the OOB data types are required to not be null."
+ + " Please use createBond() instead if you do not have OOB data to pass.");
+ }
+ return createBondInternal(transport, remoteP192Data, remoteP256Data);
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private boolean createBondInternal(int transport, @Nullable OobData remoteP192Data,
+ @Nullable OobData remoteP256Data) {
+ if (DBG) log("createBondOutOfBand()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "BT not enabled, createBondOutOfBand failed");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (NULL_MAC_ADDRESS.equals(mAddress)) {
+ Log.e(TAG, "Unable to create bond, invalid address " + mAddress);
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.createBond(this, transport, remoteP192Data, remoteP256Data,
+ mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets whether bonding was initiated locally
+ *
+ * @return true if bonding is initiated locally, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isBondingInitiatedLocally() {
+ if (DBG) log("isBondingInitiatedLocally()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "BT not enabled, isBondingInitiatedLocally failed");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.isBondingInitiatedLocally(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Cancel an in-progress bonding request started with {@link #createBond}.
+ *
+ * @return true on success, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean cancelBondProcess() {
+ if (DBG) log("cancelBondProcess()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot cancel Remote Device bond");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ Log.i(TAG, "cancelBondProcess() for device " + toStringForLogging()
+ + " called by pid: " + Process.myPid()
+ + " tid: " + Process.myTid());
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.cancelBondProcess(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Remove bond (pairing) with the remote device.
+ * <p>Delete the link key associated with the remote device, and
+ * immediately terminate connections to that device that require
+ * authentication and encryption.
+ *
+ * @return true on success, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean removeBond() {
+ if (DBG) log("removeBond()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot remove Remote Device bond");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ Log.i(TAG, "removeBond() for device " + toStringForLogging()
+ + " called by pid: " + Process.myPid()
+ + " tid: " + Process.myTid());
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.removeBond(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * There are several instances of IpcDataCache used in this class.
+ * BluetoothCache wraps up the common code. All caches are created with a maximum of
+ * eight entries, and the key is in the bluetooth module. The name is set to the api.
+ */
+ private static class BluetoothCache<Q, R> extends IpcDataCache<Q, R> {
+ BluetoothCache(String api, IpcDataCache.QueryHandler query) {
+ super(8, IpcDataCache.MODULE_BLUETOOTH, api, api, query);
+ }};
+
+ /**
+ * Invalidate a bluetooth cache. This method is just a short-hand wrapper that
+ * enforces the bluetooth module.
+ */
+ private static void invalidateCache(@NonNull String api) {
+ IpcDataCache.invalidateCache(IpcDataCache.MODULE_BLUETOOTH, api);
+ }
+
+ private static final IpcDataCache
+ .QueryHandler<Pair<IBluetooth, Pair<AttributionSource, BluetoothDevice>>, Integer>
+ sBluetoothBondQuery = new IpcDataCache.QueryHandler<>() {
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @Override
+ public Integer apply(Pair<IBluetooth,
+ Pair<AttributionSource, BluetoothDevice>> pairQuery) {
+ IBluetooth service = pairQuery.first;
+ AttributionSource source = pairQuery.second.first;
+ BluetoothDevice device = pairQuery.second.second;
+ if (DBG) {
+ log("getBondState(" + device.toStringForLogging() + ") uncached");
+ }
+ try {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ service.getBondState(device, source, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(BOND_NONE);
+ } catch (RemoteException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ private static final String GET_BOND_STATE_API = "BluetoothDevice_getBondState";
+
+ private static final
+ BluetoothCache<Pair<IBluetooth, Pair<AttributionSource, BluetoothDevice>>, Integer>
+ sBluetoothBondCache = new BluetoothCache<>(GET_BOND_STATE_API, sBluetoothBondQuery);
+
+ /** @hide */
+ public void disableBluetoothGetBondStateCache() {
+ sBluetoothBondCache.disableForCurrentProcess();
+ }
+
+ /** @hide */
+ public static void invalidateBluetoothGetBondStateCache() {
+ invalidateCache(GET_BOND_STATE_API);
+ }
+
+ /**
+ * Get the bond state of the remote device.
+ * <p>Possible values for the bond state are:
+ * {@link #BOND_NONE},
+ * {@link #BOND_BONDING},
+ * {@link #BOND_BONDED}.
+ *
+ * @return the bond state
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getBondState() {
+ if (DBG) log("getBondState(" + toStringForLogging() + ")");
+ final IBluetooth service = getService();
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot get bond state");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ return sBluetoothBondCache.query(
+ new Pair<>(service, new Pair<>(mAttributionSource, BluetoothDevice.this)));
+ } catch (RuntimeException e) {
+ if (!(e.getCause() instanceof TimeoutException)
+ && !(e.getCause() instanceof RemoteException)) {
+ throw e;
+ }
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return BOND_NONE;
+ }
+
+ /**
+ * Checks whether this bluetooth device is associated with CDM and meets the criteria to skip
+ * the bluetooth pairing dialog because it has been already consented by the CDM prompt.
+ *
+ * @return true if we can bond without the dialog, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean canBondWithoutDialog() {
+ if (DBG) log("canBondWithoutDialog, device: " + toStringForLogging());
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot check if we can skip pairing dialog");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.canBondWithoutDialog(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the package name of the application that initiate bonding with this device
+ *
+ * @return package name of the application, or null of no application initiate bonding with
+ * this device
+ *
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public String getPackageNameOfBondingApplication() {
+ if (DBG) log("getPackageNameOfBondingApplication()");
+ final IBluetooth service = getService();
+ final String defaultValue = null;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "BT not enabled, getPackageNameOfBondingApplication failed");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<String> recv = SynchronousResultReceiver.get();
+ service.getPackageNameOfBondingApplication(this, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED
+ })
+ public @interface ConnectionReturnValues{}
+
+ /**
+ * Connects all user enabled and supported bluetooth profiles between the local and remote
+ * device. If no profiles are user enabled (e.g. first connection), we connect all supported
+ * profiles. If the device is not already connected, this will page the device before initiating
+ * profile connections. Connection is asynchronous and you should listen to each profile's
+ * broadcast intent ACTION_CONNECTION_STATE_CHANGED to verify whether connection was successful.
+ * For example, to verify a2dp is connected, you would listen for
+ * {@link BluetoothA2dp#ACTION_CONNECTION_STATE_CHANGED}
+ *
+ * @return whether the messages were successfully sent to try to connect all profiles
+ * @throws IllegalArgumentException if the device address is invalid
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public @ConnectionReturnValues int connect() {
+ if (DBG) log("connect()");
+ if (!BluetoothAdapter.checkBluetoothAddress(getAddress())) {
+ throw new IllegalArgumentException("device cannot have an invalid address");
+ }
+ final IBluetooth service = getService();
+ final int defaultValue = BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot connect to remote device.");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.connectAllEnabledProfiles(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Disconnects all connected bluetooth profiles between the local and remote device.
+ * Disconnection is asynchronous, so you should listen to each profile's broadcast intent
+ * ACTION_CONNECTION_STATE_CHANGED to verify whether disconnection was successful. For example,
+ * to verify a2dp is disconnected, you would listen for
+ * {@link BluetoothA2dp#ACTION_CONNECTION_STATE_CHANGED}. Once all profiles have disconnected,
+ * the ACL link should come down and {@link #ACTION_ACL_DISCONNECTED} should be broadcast.
+ * <p>
+ * In the rare event that one or more profiles fail to disconnect, call this method again to
+ * send another request to disconnect each connected profile.
+ *
+ * @return whether the messages were successfully sent to try to disconnect all profiles
+ * @throws IllegalArgumentException if the device address is invalid
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionReturnValues int disconnect() {
+ if (DBG) log("disconnect()");
+ if (!BluetoothAdapter.checkBluetoothAddress(getAddress())) {
+ throw new IllegalArgumentException("device cannot have an invalid address");
+ }
+ final IBluetooth service = getService();
+ final int defaultValue = BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot disconnect to remote device.");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.disconnectAllEnabledProfiles(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns whether there is an open connection to this device.
+ *
+ * @return True if there is at least one open connection to this device.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isConnected() {
+ if (DBG) log("isConnected()");
+ final IBluetooth service = getService();
+ final int defaultValue = CONNECTION_STATE_DISCONNECTED;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue)
+ != CONNECTION_STATE_DISCONNECTED;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ // BT is not enabled, we cannot be connected.
+ return false;
+ }
+
+ /**
+ * Returns the ACL connection handle associated with an open connection to
+ * this device on the given transport.
+ *
+ * This handle is a unique identifier for the connection while it remains
+ * active. Refer to the Bluetooth Core Specification Version 5.4 Vol 4 Part E
+ * Section 5.3.1 Controller Handles for details.
+ *
+ * @return the ACL handle, or {@link BluetoothDevice#ERROR} if no connection currently exists on
+ * the given transport.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public int getConnectionHandle(@Transport int transport) {
+ if (DBG) {
+ log("getConnectionHandle()");
+ }
+ final IBluetooth service = getService();
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) {
+ log(Log.getStackTraceString(new Throwable()));
+ }
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionHandle(this, transport, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(-1);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ // BT is not enabled, we cannot be connected.
+ return BluetoothDevice.ERROR;
+ }
+
+ /**
+ * Returns whether there is an open connection to this device
+ * that has been encrypted.
+ *
+ * @return True if there is at least one encrypted connection to this device.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isEncrypted() {
+ if (DBG) log("isEncrypted()");
+ final IBluetooth service = getService();
+ final int defaultValue = CONNECTION_STATE_DISCONNECTED;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue)
+ > CONNECTION_STATE_CONNECTED;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ // BT is not enabled, we cannot be encrypted.
+ return false;
+ }
+
+ /**
+ * Get the Bluetooth class of the remote device.
+ *
+ * @return Bluetooth class object, or null on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothClass getBluetoothClass() {
+ if (DBG) log("getBluetoothClass()");
+ final IBluetooth service = getService();
+ final int defaultValue = 0;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot get Bluetooth Class");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getRemoteClass(this, mAttributionSource, recv);
+ int classInt = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ if (classInt == BluetoothClass.ERROR) return null;
+ return new BluetoothClass(classInt);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the supported features (UUIDs) of the remote device.
+ *
+ * <p>This method does not start a service discovery procedure to retrieve the UUIDs
+ * from the remote device. Instead, the local cached copy of the service
+ * UUIDs are returned.
+ * <p>Use {@link #fetchUuidsWithSdp} if fresh UUIDs are desired.
+ *
+ * @return the supported features (UUIDs) of the remote device, or null on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public ParcelUuid[] getUuids() {
+ if (DBG) log("getUuids()");
+ final IBluetooth service = getService();
+ final ParcelUuid[] defaultValue = null;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot get remote device Uuids");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<List<ParcelUuid>> recv =
+ SynchronousResultReceiver.get();
+ service.getRemoteUuids(this, mAttributionSource, recv);
+ List<ParcelUuid> parcels = recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(null);
+ return parcels != null ? parcels.toArray(new ParcelUuid[parcels.size()]) : null;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Perform a service discovery on the remote device to get the UUIDs supported.
+ *
+ * <p>This API is asynchronous and {@link #ACTION_UUID} intent is sent,
+ * with the UUIDs supported by the remote end. If there is an error
+ * in getting the SDP records or if the process takes a long time, or the device is bonding and
+ * we have its UUIDs cached, {@link #ACTION_UUID} intent is sent with the UUIDs that is
+ * currently present in the cache. Clients should use the {@link #getUuids} to get UUIDs
+ * if service discovery is not to be performed. If there is an ongoing bonding process,
+ * service discovery or device inquiry, the request will be queued.
+ *
+ * @return False if the check fails, True if the process of initiating an ACL connection
+ * to the remote device was started or cached UUIDs will be broadcast.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean fetchUuidsWithSdp() {
+ return fetchUuidsWithSdp(TRANSPORT_AUTO);
+ }
+
+ /**
+ * Perform a service discovery on the remote device to get the UUIDs supported with the
+ * specific transport.
+ *
+ * <p>This API is asynchronous and {@link #ACTION_UUID} intent is sent,
+ * with the UUIDs supported by the remote end. If there is an error
+ * in getting the SDP or GATT records or if the process takes a long time, or the device
+ * is bonding and we have its UUIDs cached, {@link #ACTION_UUID} intent is sent with the
+ * UUIDs that is currently present in the cache. Clients should use the {@link #getUuids}
+ * to get UUIDs if service discovery is not to be performed. If there is an ongoing bonding
+ * process, service discovery or device inquiry, the request will be queued.
+ *
+ * @param transport - provide type of transport (e.g. LE or Classic).
+ * @return False if the check fails, True if the process of initiating an ACL connection
+ * to the remote device was started or cached UUIDs will be broadcast with the specific
+ * transport.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean fetchUuidsWithSdp(@Transport int transport) {
+ if (DBG) log("fetchUuidsWithSdp()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot fetchUuidsWithSdp");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.fetchRemoteUuids(this, transport, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Perform a service discovery on the remote device to get the SDP records associated
+ * with the specified UUID.
+ *
+ * <p>This API is asynchronous and {@link #ACTION_SDP_RECORD} intent is sent,
+ * with the SDP records found on the remote end. If there is an error
+ * in getting the SDP records or if the process takes a long time,
+ * {@link #ACTION_SDP_RECORD} intent is sent with an status value in
+ * {@link #EXTRA_SDP_SEARCH_STATUS} different from 0.
+ * Detailed status error codes can be found by members of the Bluetooth package in
+ * the AbstractionLayer class.
+ * <p>The SDP record data will be stored in the intent as {@link #EXTRA_SDP_RECORD}.
+ * The object type will match one of the SdpXxxRecord types, depending on the UUID searched
+ * for.
+ *
+ * @return False if the check fails, True if the process
+ * of initiating an ACL connection to the remote device
+ * was started.
+ */
+ /** @hide */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sdpSearch(ParcelUuid uuid) {
+ if (DBG) log("sdpSearch()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot query remote device sdp records");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.sdpSearch(this, uuid, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set the pin during pairing when the pairing method is {@link #PAIRING_VARIANT_PIN}
+ *
+ * @return true pin has been set false for error
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setPin(byte[] pin) {
+ if (DBG) log("setPin()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot set Remote Device pin");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setPin(this, true, pin.length, pin, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set the pin during pairing when the pairing method is {@link #PAIRING_VARIANT_PIN}
+ *
+ * @return true pin has been set false for error
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setPin(@NonNull String pin) {
+ byte[] pinBytes = convertPinToBytes(pin);
+ if (pinBytes == null) {
+ return false;
+ }
+ return setPin(pinBytes);
+ }
+
+ /**
+ * Confirm passkey for {@link #PAIRING_VARIANT_PASSKEY_CONFIRMATION} pairing.
+ *
+ * @return true confirmation has been sent out false for error
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setPairingConfirmation(boolean confirm) {
+ if (DBG) log("setPairingConfirmation()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot set pairing confirmation");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setPairingConfirmation(this, confirm, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ boolean isBluetoothEnabled() {
+ boolean ret = false;
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter != null && adapter.isEnabled()) {
+ ret = true;
+ }
+ return ret;
+ }
+
+ /**
+ * Gets whether the phonebook access is allowed for this bluetooth device
+ *
+ * @return Whether the phonebook access is allowed to this device. Can be {@link
+ * #ACCESS_UNKNOWN}, {@link #ACCESS_ALLOWED} or {@link #ACCESS_REJECTED}.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @AccessPermission int getPhonebookAccessPermission() {
+ if (DBG) log("getPhonebookAccessPermission()");
+ final IBluetooth service = getService();
+ final int defaultValue = ACCESS_UNKNOWN;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getPhonebookAccessPermission(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets whether the {@link BluetoothDevice} enters silence mode. Audio will not
+ * be routed to the {@link BluetoothDevice} if set to {@code true}.
+ *
+ * When the {@link BluetoothDevice} enters silence mode, and the {@link BluetoothDevice}
+ * is an active device (for A2DP or HFP), the active device for that profile
+ * will be set to null.
+ * If the {@link BluetoothDevice} exits silence mode while the A2DP or HFP
+ * active device is null, the {@link BluetoothDevice} will be set as the
+ * active device for that profile.
+ * If the {@link BluetoothDevice} is disconnected, it exits silence mode.
+ * If the {@link BluetoothDevice} is set as the active device for A2DP or
+ * HFP, while silence mode is enabled, then the device will exit silence mode.
+ * If the {@link BluetoothDevice} is in silence mode, AVRCP position change
+ * event and HFP AG indicators will be disabled.
+ * If the {@link BluetoothDevice} is not connected with A2DP or HFP, it cannot
+ * enter silence mode.
+ *
+ * @param silence true to enter silence mode, false to exit
+ * @return true on success, false on error.
+ * @throws IllegalStateException if Bluetooth is not turned ON.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setSilenceMode(boolean silence) {
+ if (DBG) log("setSilenceMode()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ throw new IllegalStateException("Bluetooth is not turned ON");
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setSilenceMode(this, silence, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Check whether the {@link BluetoothDevice} is in silence mode
+ *
+ * @return true on device in silence mode, otherwise false.
+ * @throws IllegalStateException if Bluetooth is not turned ON.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean isInSilenceMode() {
+ if (DBG) log("isInSilenceMode()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ throw new IllegalStateException("Bluetooth is not turned ON");
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.getSilenceMode(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets whether the phonebook access is allowed to this device.
+ *
+ * @param value Can be {@link #ACCESS_UNKNOWN}, {@link #ACCESS_ALLOWED} or {@link
+ * #ACCESS_REJECTED}.
+ * @return Whether the value has been successfully set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setPhonebookAccessPermission(@AccessPermission int value) {
+ if (DBG) log("setPhonebookAccessPermission()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setPhonebookAccessPermission(this, value, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets whether message access is allowed to this bluetooth device
+ *
+ * @return Whether the message access is allowed to this device.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @AccessPermission int getMessageAccessPermission() {
+ if (DBG) log("getMessageAccessPermission()");
+ final IBluetooth service = getService();
+ final int defaultValue = ACCESS_UNKNOWN;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getMessageAccessPermission(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets whether the message access is allowed to this device.
+ *
+ * @param value Can be {@link #ACCESS_UNKNOWN} if the device is unbonded,
+ * {@link #ACCESS_ALLOWED} if the permission is being granted, or {@link #ACCESS_REJECTED} if
+ * the permission is not being granted.
+ * @return Whether the value has been successfully set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setMessageAccessPermission(@AccessPermission int value) {
+ // Validates param value is one of the accepted constants
+ if (value != ACCESS_ALLOWED && value != ACCESS_REJECTED && value != ACCESS_UNKNOWN) {
+ throw new IllegalArgumentException(value + "is not a valid AccessPermission value");
+ }
+ if (DBG) log("setMessageAccessPermission()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setMessageAccessPermission(this, value, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets whether sim access is allowed for this bluetooth device
+ *
+ * @return Whether the Sim access is allowed to this device.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @AccessPermission int getSimAccessPermission() {
+ if (DBG) log("getSimAccessPermission()");
+ final IBluetooth service = getService();
+ final int defaultValue = ACCESS_UNKNOWN;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getSimAccessPermission(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets whether the Sim access is allowed to this device.
+ *
+ * @param value Can be {@link #ACCESS_UNKNOWN} if the device is unbonded,
+ * {@link #ACCESS_ALLOWED} if the permission is being granted, or {@link #ACCESS_REJECTED} if
+ * the permission is not being granted.
+ * @return Whether the value has been successfully set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setSimAccessPermission(int value) {
+ if (DBG) log("setSimAccessPermission()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setSimAccessPermission(this, value, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Create an RFCOMM {@link BluetoothSocket} ready to start a secure
+ * outgoing connection to this remote device on given channel.
+ * <p>The remote device will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p> Use this socket only if an authenticated socket link is possible.
+ * Authentication refers to the authentication of the link key to
+ * prevent person-in-the-middle type of attacks.
+ * For example, for Bluetooth 2.1 devices, if any of the devices does not
+ * have an input and output capability or just has the ability to
+ * display a numeric key, a secure socket connection is not possible.
+ * In such a case, use {@link createInsecureRfcommSocket}.
+ * For more details, refer to the Security Model section 5.2 (vol 3) of
+ * Bluetooth Core Specification version 2.1 + EDR.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection.
+ * <p>Valid RFCOMM channels are in range 1 to 30.
+ *
+ * @param channel RFCOMM channel to connect to
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createRfcommSocket(int channel) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, true, true, this, channel,
+ null);
+ }
+
+ /**
+ * Create an L2cap {@link BluetoothSocket} ready to start a secure
+ * outgoing connection to this remote device on given channel.
+ * <p>The remote device will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p> Use this socket only if an authenticated socket link is possible.
+ * Authentication refers to the authentication of the link key to
+ * prevent person-in-the-middle type of attacks.
+ * For example, for Bluetooth 2.1 devices, if any of the devices does not
+ * have an input and output capability or just has the ability to
+ * display a numeric key, a secure socket connection is not possible.
+ * In such a case, use {@link createInsecureRfcommSocket}.
+ * For more details, refer to the Security Model section 5.2 (vol 3) of
+ * Bluetooth Core Specification version 2.1 + EDR.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection.
+ * <p>Valid L2CAP PSM channels are in range 1 to 2^16.
+ *
+ * @param channel L2cap PSM/channel to connect to
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createL2capSocket(int channel) throws IOException {
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP, -1, true, true, this, channel,
+ null);
+ }
+
+ /**
+ * Create an L2cap {@link BluetoothSocket} ready to start an insecure
+ * outgoing connection to this remote device on given channel.
+ * <p>The remote device will be not authenticated and communication on this
+ * socket will not be encrypted.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection.
+ * <p>Valid L2CAP PSM channels are in range 1 to 2^16.
+ *
+ * @param channel L2cap PSM/channel to connect to
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createInsecureL2capSocket(int channel) throws IOException {
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP, -1, false, false, this, channel,
+ null);
+ }
+
+ /**
+ * Create an RFCOMM {@link BluetoothSocket} ready to start a secure
+ * outgoing connection to this remote device using SDP lookup of uuid.
+ * <p>This is designed to be used with {@link
+ * BluetoothAdapter#listenUsingRfcommWithServiceRecord} for peer-peer
+ * Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection. This will also perform an SDP lookup of the given uuid to
+ * determine which channel to connect to.
+ * <p>The remote device will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p> Use this socket only if an authenticated socket link is possible.
+ * Authentication refers to the authentication of the link key to
+ * prevent person-in-the-middle type of attacks.
+ * For example, for Bluetooth 2.1 devices, if any of the devices does not
+ * have an input and output capability or just has the ability to
+ * display a numeric key, a secure socket connection is not possible.
+ * In such a case, use {@link #createInsecureRfcommSocketToServiceRecord}.
+ * For more details, refer to the Security Model section 5.2 (vol 3) of
+ * Bluetooth Core Specification version 2.1 + EDR.
+ * <p>Hint: If you are connecting to a Bluetooth serial board then try
+ * using the well-known SPP UUID 00001101-0000-1000-8000-00805F9B34FB.
+ * However if you are connecting to an Android peer then please generate
+ * your own unique UUID.
+ *
+ * @param uuid service record uuid to lookup RFCOMM channel
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+
+ return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, true, true, this, -1,
+ new ParcelUuid(uuid));
+ }
+
+ /**
+ * Create an RFCOMM {@link BluetoothSocket} socket ready to start an insecure
+ * outgoing connection to this remote device using SDP lookup of uuid.
+ * <p> The communication channel will not have an authenticated link key
+ * i.e it will be subject to person-in-the-middle attacks. For Bluetooth 2.1
+ * devices, the link key will be encrypted, as encryption is mandatory.
+ * For legacy devices (pre Bluetooth 2.1 devices) the link key will
+ * be not be encrypted. Use {@link #createRfcommSocketToServiceRecord} if an
+ * encrypted and authenticated communication channel is desired.
+ * <p>This is designed to be used with {@link
+ * BluetoothAdapter#listenUsingInsecureRfcommWithServiceRecord} for peer-peer
+ * Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection. This will also perform an SDP lookup of the given uuid to
+ * determine which channel to connect to.
+ * <p>The remote device will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p>Hint: If you are connecting to a Bluetooth serial board then try
+ * using the well-known SPP UUID 00001101-0000-1000-8000-00805F9B34FB.
+ * However if you are connecting to an Android peer then please generate
+ * your own unique UUID.
+ *
+ * @param uuid service record uuid to lookup RFCOMM channel
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createInsecureRfcommSocketToServiceRecord(UUID uuid) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, false, false, this, -1,
+ new ParcelUuid(uuid));
+ }
+
+ /**
+ * Construct an insecure RFCOMM socket ready to start an outgoing
+ * connection.
+ * Call #connect on the returned #BluetoothSocket to begin the connection.
+ * The remote device will not be authenticated and communication on this
+ * socket will not be encrypted.
+ *
+ * @param port remote port
+ * @return An RFCOMM BluetoothSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @UnsupportedAppUsage(publicAlternatives = "Use "
+ + "{@link #createInsecureRfcommSocketToServiceRecord} instead.")
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createInsecureRfcommSocket(int port) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, false, false, this, port,
+ null);
+ }
+
+ /**
+ * Construct a SCO socket ready to start an outgoing connection.
+ * Call #connect on the returned #BluetoothSocket to begin the connection.
+ *
+ * @return a SCO BluetoothSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createScoSocket() throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_SCO, -1, true, true, this, -1, null);
+ }
+
+ /**
+ * Check that a pin is valid and convert to byte array.
+ *
+ * Bluetooth pin's are 1 to 16 bytes of UTF-8 characters.
+ *
+ * @param pin pin as java String
+ * @return the pin code as a UTF-8 byte array, or null if it is an invalid Bluetooth pin.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static byte[] convertPinToBytes(String pin) {
+ if (pin == null) {
+ return null;
+ }
+ byte[] pinBytes;
+ try {
+ pinBytes = pin.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException uee) {
+ Log.e(TAG, "UTF-8 not supported?!?"); // this should not happen
+ return null;
+ }
+ if (pinBytes.length <= 0 || pinBytes.length > 16) {
+ return null;
+ }
+ return pinBytes;
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @throws IllegalArgumentException if callback is null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback) {
+ return (connectGatt(context, autoConnect, callback, TRANSPORT_AUTO));
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @throws IllegalArgumentException if callback is null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport) {
+ return (connectGatt(context, autoConnect, callback, transport, PHY_LE_1M_MASK));
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @param phy preferred PHY for connections to remote LE device. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}. This option does not take effect if {@code autoConnect}
+ * is set to true.
+ * @throws NullPointerException if callback is null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport, int phy) {
+ return connectGatt(context, autoConnect, callback, transport, phy, null);
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @param phy preferred PHY for connections to remote LE device. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, an d{@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}. This option does not take effect if {@code autoConnect}
+ * is set to true.
+ * @param handler The handler to use for the callback. If {@code null}, callbacks will happen on
+ * an un-specified background thread.
+ * @throws NullPointerException if callback is null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport, int phy,
+ Handler handler) {
+ return connectGatt(context, autoConnect, callback, transport, false, phy, handler);
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @param opportunistic Whether this GATT client is opportunistic. An opportunistic GATT client
+ * does not hold a GATT connection. It automatically disconnects when no other GATT connections
+ * are active for the remote device.
+ * @param phy preferred PHY for connections to remote LE device. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, an d{@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}. This option does not take effect if {@code autoConnect}
+ * is set to true.
+ * @param handler The handler to use for the callback. If {@code null}, callbacks will happen on
+ * an un-specified background thread.
+ * @return A BluetoothGatt instance. You can use BluetoothGatt to conduct GATT client
+ * operations.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport,
+ boolean opportunistic, int phy, Handler handler) {
+ if (callback == null) {
+ throw new NullPointerException("callback is null");
+ }
+
+ // TODO(Bluetooth) check whether platform support BLE
+ // Do the check here or in GattServer?
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ IBluetoothManager managerService = adapter.getBluetoothManager();
+ try {
+ IBluetoothGatt iGatt = managerService.getBluetoothGatt();
+ if (iGatt == null) {
+ // BLE is not supported
+ return null;
+ } else if (NULL_MAC_ADDRESS.equals(mAddress)) {
+ Log.e(TAG, "Unable to connect gatt, invalid address " + mAddress);
+ return null;
+ }
+ BluetoothGatt gatt = new BluetoothGatt(
+ iGatt, this, transport, opportunistic, phy, mAttributionSource);
+ gatt.connect(autoConnect, callback, handler);
+ return gatt;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /**
+ * Create a Bluetooth L2CAP Connection-oriented Channel (CoC) {@link BluetoothSocket} that can
+ * be used to start a secure outgoing connection to the remote device with the same dynamic
+ * protocol/service multiplexer (PSM) value. The supported Bluetooth transport is LE only.
+ * <p>This is designed to be used with {@link BluetoothAdapter#listenUsingL2capChannel()} for
+ * peer-peer Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing connection.
+ * <p>Application using this API is responsible for obtaining PSM value from remote device.
+ * <p>The remote device will be authenticated and communication on this socket will be
+ * encrypted.
+ * <p> Use this socket if an authenticated socket link is possible. Authentication refers
+ * to the authentication of the link key to prevent person-in-the-middle type of attacks.
+ *
+ * @param psm dynamic PSM value from remote device
+ * @return a CoC #BluetoothSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public @NonNull BluetoothSocket createL2capChannel(int psm) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "createL2capChannel: Bluetooth is not enabled");
+ throw new IOException();
+ }
+ if (DBG) Log.d(TAG, "createL2capChannel: psm=" + psm);
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP_LE, -1, true, true, this, psm,
+ null);
+ }
+
+ /**
+ * Create a Bluetooth L2CAP Connection-oriented Channel (CoC) {@link BluetoothSocket} that can
+ * be used to start a secure outgoing connection to the remote device with the same dynamic
+ * protocol/service multiplexer (PSM) value. The supported Bluetooth transport is LE only.
+ * <p>This is designed to be used with {@link
+ * BluetoothAdapter#listenUsingInsecureL2capChannel()} for peer-peer Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing connection.
+ * <p>Application using this API is responsible for obtaining PSM value from remote device.
+ * <p> The communication channel may not have an authenticated link key, i.e. it may be subject
+ * to person-in-the-middle attacks. Use {@link #createL2capChannel(int)} if an encrypted and
+ * authenticated communication channel is possible.
+ *
+ * @param psm dynamic PSM value from remote device
+ * @return a CoC #BluetoothSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public @NonNull BluetoothSocket createInsecureL2capChannel(int psm) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "createInsecureL2capChannel: Bluetooth is not enabled");
+ throw new IOException();
+ }
+ if (DBG) {
+ Log.d(TAG, "createInsecureL2capChannel: psm=" + psm);
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP_LE, -1, false, false, this, psm,
+ null);
+ }
+
+ /**
+ * Set a keyed metadata of this {@link BluetoothDevice} to a
+ * {@link String} value.
+ * Only bonded devices's metadata will be persisted across Bluetooth
+ * restart.
+ * Metadata will be removed when the device's bond state is moved to
+ * {@link #BOND_NONE}.
+ *
+ * @param key must be within the list of BluetoothDevice.METADATA_*
+ * @param value a byte array data to set for key. Must be less than
+ * {@link BluetoothAdapter#METADATA_MAX_LENGTH} characters in length
+ * @return true on success, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setMetadata(@MetadataKey int key, @NonNull byte[] value) {
+ if (DBG) log("setMetadata()");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot set metadata");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (value.length > METADATA_MAX_LENGTH) {
+ throw new IllegalArgumentException("value length is " + value.length
+ + ", should not over " + METADATA_MAX_LENGTH);
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setMetadata(this, key, value, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get a keyed metadata for this {@link BluetoothDevice} as {@link String}
+ *
+ * @param key must be within the list of BluetoothDevice.METADATA_*
+ * @return Metadata of the key as byte array, null on error or not found
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public byte[] getMetadata(@MetadataKey int key) {
+ if (DBG) log("getMetadata()");
+ final IBluetooth service = getService();
+ final byte[] defaultValue = null;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot get metadata");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<byte[]> recv = SynchronousResultReceiver.get();
+ service.getMetadata(this, key, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the maxinum metadata key ID.
+ *
+ * @return the last supported metadata key
+ * @hide
+ */
+ public static @MetadataKey int getMaxMetadataKey() {
+ return METADATA_MAX_KEY;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = { "FEATURE_" },
+ value = {
+ BluetoothStatusCodes.FEATURE_NOT_CONFIGURED,
+ BluetoothStatusCodes.FEATURE_SUPPORTED,
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED,
+ }
+ )
+
+ public @interface AudioPolicyRemoteSupport {}
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED,
+ })
+ public @interface AudioPolicyReturnValues{}
+
+ /**
+ * Returns whether the audio policy feature is supported by
+ * both the local and the remote device.
+ * This is configured during initiating the connection between the devices through
+ * one of the transport protocols (e.g. HFP Vendor specific protocol). So if the API
+ * is invoked before this initial configuration is completed, it returns
+ * {@link BluetoothStatusCodes#FEATURE_NOT_CONFIGURED} to indicate the remote
+ * device has not yet relayed this information. After the internal configuration,
+ * the support status will be set to either
+ * {@link BluetoothStatusCodes#FEATURE_NOT_SUPPORTED} or
+ * {@link BluetoothStatusCodes#FEATURE_SUPPORTED}.
+ * The rest of the APIs related to this feature in both {@link BluetoothDevice}
+ * and {@link BluetoothSinkAudioPolicy} should be invoked only after getting a
+ * {@link BluetoothStatusCodes#FEATURE_SUPPORTED} response from this API.
+ * <p>Note that this API is intended to be used by a client device to send these requests
+ * to the server represented by this BluetoothDevice object.
+ *
+ * @return if call audio policy feature is supported by both local and remote
+ * device or not
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @AudioPolicyRemoteSupport int isRequestAudioPolicyAsSinkSupported() {
+ if (DBG) log("isRequestAudioPolicyAsSinkSupported()");
+ final IBluetooth service = getService();
+ final int defaultValue = BluetoothStatusCodes.FEATURE_NOT_CONFIGURED;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot retrieve audio policy support status.");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.isRequestAudioPolicyAsSinkSupported(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets call audio preferences and sends them to the remote device.
+ * <p>Note that the caller should check if the feature is supported by
+ * invoking {@link BluetoothDevice#isRequestAudioPolicyAsSinkSupported} first.
+ * <p>This API will throw an exception if the feature is not supported but still
+ * invoked.
+ * <p>Note that this API is intended to be used by a client device to send these requests
+ * to the server represented by this BluetoothDevice object.
+ *
+ * @param policies call audio policy preferences
+ * @return whether audio policy was requested successfully or not
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @AudioPolicyReturnValues int requestAudioPolicyAsSink(
+ @NonNull BluetoothSinkAudioPolicy policies) {
+ if (DBG) log("requestAudioPolicyAsSink");
+ final IBluetooth service = getService();
+ final int defaultValue = BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot set Audio Policy.");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.requestAudioPolicyAsSink(this, policies, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the call audio preferences for the remote device.
+ * <p>Note that the caller should check if the feature is supported by
+ * invoking {@link BluetoothDevice#isRequestAudioPolicyAsSinkSupported} first.
+ * <p>This API will throw an exception if the feature is not supported but still
+ * invoked.
+ * <p>This API will return null if
+ * 1. The bleutooth service is not started yet,
+ * 2. It is invoked for a device which is not bonded, or
+ * 3. The used transport, for example, HFP Client profile is not enabled or
+ * connected yet.
+ * <p>Note that this API is intended to be used by a client device to send these requests
+ * to the server represented by this BluetoothDevice object.
+ *
+ * @return call audio policy as {@link BluetoothSinkAudioPolicy} object
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Nullable BluetoothSinkAudioPolicy getRequestedAudioPolicyAsSink() {
+ if (DBG) log("getRequestedAudioPolicyAsSink");
+ final IBluetooth service = getService();
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot get Audio Policy.");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<BluetoothSinkAudioPolicy>
+ recv = SynchronousResultReceiver.get();
+ service.getRequestedAudioPolicyAsSink(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Enable or disable audio low latency for this {@link BluetoothDevice}.
+ *
+ * @param allowed true if low latency is allowed, false if low latency is disallowed.
+ * @return true if the value is successfully set,
+ * false if there is a error when setting the value.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setLowLatencyAudioAllowed(boolean allowed) {
+ if (DBG) log("setLowLatencyAudioAllowed(" + allowed + ")");
+ final IBluetooth service = getService();
+ final boolean defaultValue = false;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot allow low latency");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.allowLowLatencyAudio(allowed, this, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothDevicePicker.java b/android-34/android/bluetooth/BluetoothDevicePicker.java
new file mode 100644
index 0000000..e5a6000
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothDevicePicker.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+
+/**
+ * A helper to show a system "Device Picker" activity to the user.
+ *
+ * @hide
+ */
+@SystemApi
+public interface BluetoothDevicePicker {
+
+ /**
+ * Extra for filter type used with {@link #ACTION_LAUNCH}.
+ * The value must be a boolean indicating whether the device should need authentication or not.
+ */
+ @SuppressLint("ActionValue")
+ String EXTRA_NEED_AUTH = "android.bluetooth.devicepicker.extra.NEED_AUTH";
+
+ /**
+ * Extra for filter type used with {@link #ACTION_LAUNCH}.
+ * This extra must contain the filter type that will be applied to the device list.
+ * Possible values are {@link #FILTER_TYPE_ALL}, {@link #FILTER_TYPE_AUDIO},
+ * {@link #FILTER_TYPE_TRANSFER}, {@link #FILTER_TYPE_PANU}, and {@link #FILTER_TYPE_NAP}.
+ */
+ @SuppressLint("ActionValue")
+ String EXTRA_FILTER_TYPE = "android.bluetooth.devicepicker.extra.FILTER_TYPE";
+
+ /**
+ * Extra for filter type used with {@link #ACTION_LAUNCH}.
+ * This extra must contain the package name that called {@link #ACTION_LAUNCH}.
+ */
+ @SuppressLint("ActionValue")
+ String EXTRA_LAUNCH_PACKAGE = "android.bluetooth.devicepicker.extra.LAUNCH_PACKAGE";
+
+ /**
+ * Extra for filter type used with {@link #ACTION_LAUNCH}.
+ * This extra must contain the class name that called {@link #ACTION_LAUNCH}.
+ */
+ @SuppressLint("ActionValue")
+ String EXTRA_LAUNCH_CLASS = "android.bluetooth.devicepicker.extra.DEVICE_PICKER_LAUNCH_CLASS";
+
+ /**
+ * Broadcast when one BT device is selected from BT device picker screen.
+ * Selected {@link BluetoothDevice} is returned in extra data named
+ * {@link BluetoothDevice#EXTRA_DEVICE}.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ String ACTION_DEVICE_SELECTED = "android.bluetooth.devicepicker.action.DEVICE_SELECTED";
+
+ /**
+ * Broadcast when someone want to select one BT device from devices list.
+ * This intent contains below extra data:
+ * - {@link #EXTRA_NEED_AUTH} (boolean): if need authentication
+ * - {@link #EXTRA_FILTER_TYPE} (int): what kinds of device should be
+ * listed
+ * - {@link #EXTRA_LAUNCH_PACKAGE} (string): where(which package) this
+ * intent come from
+ * - {@link #EXTRA_LAUNCH_CLASS} (string): where(which class) this intent
+ * come from
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ String ACTION_LAUNCH = "android.bluetooth.devicepicker.action.LAUNCH";
+
+ /** Ask device picker to show all kinds of BT devices */
+ int FILTER_TYPE_ALL = 0;
+ /** Ask device picker to show BT devices that support AUDIO profiles */
+ int FILTER_TYPE_AUDIO = 1;
+ /** Ask device picker to show BT devices that support Object Transfer */
+ int FILTER_TYPE_TRANSFER = 2;
+ /**
+ * Ask device picker to show BT devices that support
+ * Personal Area Networking User (PANU) profile
+ */
+ int FILTER_TYPE_PANU = 3;
+ /** Ask device picker to show BT devices that support Network Access Point (NAP) profile */
+ int FILTER_TYPE_NAP = 4;
+}
diff --git a/android-34/android/bluetooth/BluetoothFrameworkInitializer.java b/android-34/android/bluetooth/BluetoothFrameworkInitializer.java
new file mode 100644
index 0000000..89eebaf
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothFrameworkInitializer.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+import android.os.BluetoothServiceManager;
+
+import java.util.function.Consumer;
+
+/**
+ * Class for performing registration for Bluetooth service.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class BluetoothFrameworkInitializer {
+ private BluetoothFrameworkInitializer() {}
+
+ private static volatile BluetoothServiceManager sBluetoothServiceManager;
+ private static volatile Consumer<Context> sBinderCallsStatsInitializer;
+
+ /**
+ * Sets an instance of {@link BluetoothServiceManager} that allows
+ * the bluetooth mainline module to register/obtain bluetooth binder services. This is called
+ * by the platform during the system initialization.
+ *
+ * @param bluetoothServiceManager instance of {@link BluetoothServiceManager} that allows
+ * the bluetooth mainline module to register/obtain bluetoothd binder services.
+ */
+ public static void setBluetoothServiceManager(
+ @NonNull BluetoothServiceManager bluetoothServiceManager) {
+ if (sBluetoothServiceManager != null) {
+ throw new IllegalStateException("setBluetoothServiceManager called twice!");
+ }
+
+ if (bluetoothServiceManager == null) {
+ throw new IllegalArgumentException("bluetoothServiceManager must not be null");
+ }
+
+ sBluetoothServiceManager = bluetoothServiceManager;
+ }
+
+ /** @hide */
+ public static BluetoothServiceManager getBluetoothServiceManager() {
+ return sBluetoothServiceManager;
+ }
+
+ /**
+ * Called by {@link ActivityThread}'s static initializer to set the callback enabling Bluetooth
+ * {@link BinderCallsStats} registeration.
+ *
+ * @param binderCallsStatsConsumer called by bluetooth service to create a new binder calls
+ * stats observer
+ */
+ public static void setBinderCallsStatsInitializer(
+ @NonNull Consumer<Context> binderCallsStatsConsumer) {
+ if (sBinderCallsStatsInitializer != null) {
+ throw new IllegalStateException("setBinderCallsStatsInitializer called twice!");
+ }
+
+ if (binderCallsStatsConsumer == null) {
+ throw new IllegalArgumentException("binderCallsStatsConsumer must not be null");
+ }
+
+ sBinderCallsStatsInitializer = binderCallsStatsConsumer;
+ }
+
+ /** @hide */
+ public static void initializeBinderCallsStats(Context context) {
+ if (sBinderCallsStatsInitializer == null) {
+ throw new IllegalStateException("sBinderCallsStatsInitializer has not been set");
+ }
+ sBinderCallsStatsInitializer.accept(context);
+ }
+
+ /**
+ * Called by {@link SystemServiceRegistry}'s static initializer and registers BT service
+ * to {@link Context}, so that {@link Context#getSystemService} can return them.
+ *
+ * @throws IllegalStateException if this is called from anywhere besides
+ * {@link SystemServiceRegistry}
+ */
+ public static void registerServiceWrappers() {
+ SystemServiceRegistry.registerContextAwareService(Context.BLUETOOTH_SERVICE,
+ BluetoothManager.class, context -> new BluetoothManager(context));
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothGatt.java b/android-34/android/bluetooth/BluetoothGatt.java
new file mode 100644
index 0000000..ca5dce8
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothGatt.java
@@ -0,0 +1,2066 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothGattCharacteristic.WriteType;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.os.Build;
+import android.os.Handler;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Public API for the Bluetooth GATT Profile.
+ *
+ * <p>This class provides Bluetooth GATT functionality to enable communication
+ * with Bluetooth Smart or Smart Ready devices.
+ *
+ * <p>To connect to a remote peripheral device, create a {@link BluetoothGattCallback}
+ * and call {@link BluetoothDevice#connectGatt} to get a instance of this class.
+ * GATT capable devices can be discovered using the Bluetooth device discovery or BLE
+ * scan process.
+ */
+public final class BluetoothGatt implements BluetoothProfile {
+ private static final String TAG = "BluetoothGatt";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ @UnsupportedAppUsage
+ private IBluetoothGatt mService;
+ @UnsupportedAppUsage
+ private volatile BluetoothGattCallback mCallback;
+ private Handler mHandler;
+ @UnsupportedAppUsage
+ private int mClientIf;
+ private BluetoothDevice mDevice;
+ @UnsupportedAppUsage
+ private boolean mAutoConnect;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private int mAuthRetryState;
+ private int mConnState;
+ private final Object mStateLock = new Object();
+ private final Object mDeviceBusyLock = new Object();
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Boolean mDeviceBusy = false;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mTransport;
+ private int mPhy;
+ private boolean mOpportunistic;
+ private final AttributionSource mAttributionSource;
+
+ private static final int AUTH_RETRY_STATE_IDLE = 0;
+ private static final int AUTH_RETRY_STATE_NO_MITM = 1;
+ private static final int AUTH_RETRY_STATE_MITM = 2;
+
+ private static final int CONN_STATE_IDLE = 0;
+ private static final int CONN_STATE_CONNECTING = 1;
+ private static final int CONN_STATE_CONNECTED = 2;
+ private static final int CONN_STATE_DISCONNECTING = 3;
+ private static final int CONN_STATE_CLOSED = 4;
+
+ private static final int WRITE_CHARACTERISTIC_MAX_RETRIES = 5;
+ private static final int WRITE_CHARACTERISTIC_TIME_TO_WAIT = 10; // milliseconds
+
+ private List<BluetoothGattService> mServices;
+
+ /** A GATT operation completed successfully */
+ public static final int GATT_SUCCESS = 0;
+
+ /** GATT read operation is not permitted */
+ public static final int GATT_READ_NOT_PERMITTED = 0x2;
+
+ /** GATT write operation is not permitted */
+ public static final int GATT_WRITE_NOT_PERMITTED = 0x3;
+
+ /** Insufficient authentication for a given operation */
+ public static final int GATT_INSUFFICIENT_AUTHENTICATION = 0x5;
+
+ /** The given request is not supported */
+ public static final int GATT_REQUEST_NOT_SUPPORTED = 0x6;
+
+ /** Insufficient encryption for a given operation */
+ public static final int GATT_INSUFFICIENT_ENCRYPTION = 0xf;
+
+ /** A read or write operation was requested with an invalid offset */
+ public static final int GATT_INVALID_OFFSET = 0x7;
+
+ /** Insufficient authorization for a given operation */
+ public static final int GATT_INSUFFICIENT_AUTHORIZATION = 0x8;
+
+ /** A write operation exceeds the maximum length of the attribute */
+ public static final int GATT_INVALID_ATTRIBUTE_LENGTH = 0xd;
+
+ /** A remote device connection is congested. */
+ public static final int GATT_CONNECTION_CONGESTED = 0x8f;
+
+ /** A GATT operation failed, errors other than the above */
+ public static final int GATT_FAILURE = 0x101;
+
+ /**
+ * Connection parameter update - Use the connection parameters recommended by the
+ * Bluetooth SIG. This is the default value if no connection parameter update
+ * is requested.
+ */
+ public static final int CONNECTION_PRIORITY_BALANCED = 0;
+
+ /**
+ * Connection parameter update - Request a high priority, low latency connection.
+ * An application should only request high priority connection parameters to transfer large
+ * amounts of data over LE quickly. Once the transfer is complete, the application should
+ * request {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED} connection parameters to reduce
+ * energy use.
+ */
+ public static final int CONNECTION_PRIORITY_HIGH = 1;
+
+ /** Connection parameter update - Request low power, reduced data rate connection parameters. */
+ public static final int CONNECTION_PRIORITY_LOW_POWER = 2;
+
+ /**
+ * Connection parameter update - Request the priority preferred for Digital Car Key for a
+ * lower latency connection. This connection parameter will consume more power than
+ * {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, so it is recommended that apps do not use
+ * this unless it specifically fits their use case.
+ */
+ public static final int CONNECTION_PRIORITY_DCK = 3;
+
+ /**
+ * Connection subrate request - Balanced.
+ *
+ * @hide
+ */
+ public static final int SUBRATE_REQUEST_MODE_BALANCED = 0;
+
+ /**
+ * Connection subrate request - High.
+ *
+ * @hide
+ */
+ public static final int SUBRATE_REQUEST_MODE_HIGH = 1;
+
+ /**
+ * Connection Subrate Request - Low Power.
+ *
+ * @hide
+ */
+ public static final int SUBRATE_REQUEST_MODE_LOW_POWER = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"SUBRATE_REQUEST_MODE"},
+ value =
+ {
+ SUBRATE_REQUEST_MODE_BALANCED,
+ SUBRATE_REQUEST_MODE_HIGH,
+ SUBRATE_REQUEST_MODE_LOW_POWER,
+ })
+ public @interface SubrateRequestMode {}
+
+ /**
+ * No authentication required.
+ *
+ * @hide
+ */
+ /*package*/ static final int AUTHENTICATION_NONE = 0;
+
+ /**
+ * Authentication requested; no person-in-the-middle protection required.
+ *
+ * @hide
+ */
+ /*package*/ static final int AUTHENTICATION_NO_MITM = 1;
+
+ /**
+ * Authentication with person-in-the-middle protection requested.
+ *
+ * @hide
+ */
+ /*package*/ static final int AUTHENTICATION_MITM = 2;
+
+ /**
+ * Bluetooth GATT callbacks. Overrides the default BluetoothGattCallback implementation.
+ */
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothGattCallback mBluetoothGattCallback =
+ new IBluetoothGattCallback.Stub() {
+ /**
+ * Application interface registered - app is ready to go
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onClientRegistered(int status, int clientIf) {
+ if (DBG) {
+ Log.d(TAG, "onClientRegistered() - status=" + status
+ + " clientIf=" + clientIf);
+ }
+ if (VDBG) {
+ synchronized (mStateLock) {
+ if (mConnState != CONN_STATE_CONNECTING) {
+ Log.e(TAG, "Bad connection state: " + mConnState);
+ }
+ }
+ }
+ mClientIf = clientIf;
+ if (status != GATT_SUCCESS) {
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onConnectionStateChange(BluetoothGatt.this,
+ GATT_FAILURE,
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+ }
+ });
+
+ synchronized (mStateLock) {
+ mConnState = CONN_STATE_IDLE;
+ }
+ return;
+ }
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ // autoConnect is inverse of "isDirect"
+ mService.clientConnect(mClientIf, mDevice.getAddress(),
+ mDevice.getAddressType(), !mAutoConnect, mTransport,
+ mOpportunistic, mPhy, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Phy update callback
+ * @hide
+ */
+ @Override
+ public void onPhyUpdate(String address, int txPhy, int rxPhy, int status) {
+ if (DBG) {
+ Log.d(TAG, "onPhyUpdate() - status=" + status
+ + " address=" + address + " txPhy=" + txPhy + " rxPhy=" + rxPhy);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onPhyUpdate(BluetoothGatt.this, txPhy, rxPhy, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Phy read callback
+ * @hide
+ */
+ @Override
+ public void onPhyRead(String address, int txPhy, int rxPhy, int status) {
+ if (DBG) {
+ Log.d(TAG, "onPhyRead() - status=" + status
+ + " address=" + address + " txPhy=" + txPhy + " rxPhy=" + rxPhy);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onPhyRead(BluetoothGatt.this, txPhy, rxPhy, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Client connection state changed
+ * @hide
+ */
+ @Override
+ public void onClientConnectionState(int status, int clientIf,
+ boolean connected, String address) {
+ if (DBG) {
+ Log.d(TAG, "onClientConnectionState() - status=" + status
+ + " clientIf=" + clientIf + " device=" + address);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+ int profileState = connected ? BluetoothProfile.STATE_CONNECTED :
+ BluetoothProfile.STATE_DISCONNECTED;
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onConnectionStateChange(BluetoothGatt.this, status,
+ profileState);
+ }
+ }
+ });
+
+ synchronized (mStateLock) {
+ if (connected) {
+ mConnState = CONN_STATE_CONNECTED;
+ } else {
+ mConnState = CONN_STATE_IDLE;
+ }
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ }
+
+ /**
+ * Remote search has been completed.
+ * The internal object structure should now reflect the state
+ * of the remote device database. Let the application know that
+ * we are done at this point.
+ * @hide
+ */
+ @Override
+ public void onSearchComplete(String address, List<BluetoothGattService> services,
+ int status) {
+ if (DBG) {
+ Log.d(TAG,
+ "onSearchComplete() = Device=" + address + " Status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ for (BluetoothGattService s : services) {
+ //services we receive don't have device set properly.
+ s.setDevice(mDevice);
+ }
+
+ mServices.addAll(services);
+
+ // Fix references to included services, as they doesn't point to right objects.
+ for (BluetoothGattService fixedService : mServices) {
+ ArrayList<BluetoothGattService> includedServices =
+ new ArrayList(fixedService.getIncludedServices());
+ fixedService.getIncludedServices().clear();
+
+ for (BluetoothGattService brokenRef : includedServices) {
+ BluetoothGattService includedService = getService(mDevice,
+ brokenRef.getUuid(), brokenRef.getInstanceId());
+ if (includedService != null) {
+ fixedService.addIncludedService(includedService);
+ } else {
+ Log.e(TAG, "Broken GATT database: can't find included service.");
+ }
+ }
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onServicesDiscovered(BluetoothGatt.this, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Remote characteristic has been read.
+ * Updates the internal value.
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onCharacteristicRead(String address, int status, int handle,
+ byte[] value) {
+ if (VDBG) {
+ Log.d(TAG, "onCharacteristicRead() - Device=" + address
+ + " handle=" + handle + " Status=" + status);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ if ((status == GATT_INSUFFICIENT_AUTHENTICATION
+ || status == GATT_INSUFFICIENT_ENCRYPTION)
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ try {
+ final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE)
+ ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM;
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.readCharacteristic(
+ mClientIf, address, handle, authReq, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ mAuthRetryState++;
+ return;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+
+ BluetoothGattCharacteristic characteristic = getCharacteristicById(mDevice,
+ handle);
+ if (characteristic == null) {
+ Log.w(TAG, "onCharacteristicRead() failed to find characteristic!");
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ if (status == 0) characteristic.setValue(value);
+ callback.onCharacteristicRead(BluetoothGatt.this, characteristic,
+ value, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Characteristic has been written to the remote device.
+ * Let the app know how we did...
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onCharacteristicWrite(String address, int status, int handle,
+ byte[] value) {
+ if (VDBG) {
+ Log.d(TAG, "onCharacteristicWrite() - Device=" + address
+ + " handle=" + handle + " Status=" + status);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ BluetoothGattCharacteristic characteristic = getCharacteristicById(mDevice,
+ handle);
+ if (characteristic == null) return;
+
+ if ((status == GATT_INSUFFICIENT_AUTHENTICATION
+ || status == GATT_INSUFFICIENT_ENCRYPTION)
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ try {
+ final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE)
+ ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM;
+ int requestStatus = BluetoothStatusCodes.ERROR_UNKNOWN;
+ for (int i = 0; i < WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ mService.writeCharacteristic(mClientIf, address, handle,
+ characteristic.getWriteType(), authReq, value,
+ mAttributionSource, recv);
+ requestStatus = recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND);
+ if (requestStatus
+ != BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY) {
+ break;
+ }
+ try {
+ Thread.sleep(WRITE_CHARACTERISTIC_TIME_TO_WAIT);
+ } catch (InterruptedException e) {
+ }
+ }
+ mAuthRetryState++;
+ return;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onCharacteristicWrite(BluetoothGatt.this, characteristic,
+ status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Remote characteristic has been updated.
+ * Updates the internal value.
+ * @hide
+ */
+ @Override
+ public void onNotify(String address, int handle, byte[] value) {
+ if (VDBG) Log.d(TAG, "onNotify() - Device=" + address + " handle=" + handle);
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ BluetoothGattCharacteristic characteristic = getCharacteristicById(mDevice,
+ handle);
+ if (characteristic == null) return;
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ characteristic.setValue(value);
+ callback.onCharacteristicChanged(BluetoothGatt.this,
+ characteristic, value);
+ }
+ }
+ });
+ }
+
+ /**
+ * Descriptor has been read.
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onDescriptorRead(String address, int status, int handle, byte[] value) {
+ if (VDBG) {
+ Log.d(TAG,
+ "onDescriptorRead() - Device=" + address + " handle=" + handle);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle);
+ if (descriptor == null) return;
+
+
+ if ((status == GATT_INSUFFICIENT_AUTHENTICATION
+ || status == GATT_INSUFFICIENT_ENCRYPTION)
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ try {
+ final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE)
+ ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM;
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.readDescriptor(mClientIf, address, handle, authReq,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ mAuthRetryState++;
+ return;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ if (status == 0) descriptor.setValue(value);
+ callback.onDescriptorRead(BluetoothGatt.this, descriptor, status,
+ value);
+ }
+ }
+ });
+ }
+
+ /**
+ * Descriptor write operation complete.
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onDescriptorWrite(String address, int status, int handle,
+ byte[] value) {
+ if (VDBG) {
+ Log.d(TAG,
+ "onDescriptorWrite() - Device=" + address + " handle=" + handle);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle);
+ if (descriptor == null) return;
+
+ if ((status == GATT_INSUFFICIENT_AUTHENTICATION
+ || status == GATT_INSUFFICIENT_ENCRYPTION)
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ try {
+ final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE)
+ ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM;
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.writeDescriptor(mClientIf, address, handle,
+ authReq, value, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ mAuthRetryState++;
+ return;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onDescriptorWrite(BluetoothGatt.this, descriptor, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Prepared write transaction completed (or aborted)
+ * @hide
+ */
+ @Override
+ public void onExecuteWrite(String address, int status) {
+ if (VDBG) {
+ Log.d(TAG, "onExecuteWrite() - Device=" + address
+ + " status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onReliableWriteCompleted(BluetoothGatt.this, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Remote device RSSI has been read
+ * @hide
+ */
+ @Override
+ public void onReadRemoteRssi(String address, int rssi, int status) {
+ if (VDBG) {
+ Log.d(TAG, "onReadRemoteRssi() - Device=" + address
+ + " rssi=" + rssi + " status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onReadRemoteRssi(BluetoothGatt.this, rssi, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Callback invoked when the MTU for a given connection changes
+ * @hide
+ */
+ @Override
+ public void onConfigureMTU(String address, int mtu, int status) {
+ if (DBG) {
+ Log.d(TAG, "onConfigureMTU() - Device=" + address
+ + " mtu=" + mtu + " status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onMtuChanged(BluetoothGatt.this, mtu, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Callback invoked when the given connection is updated
+ * @hide
+ */
+ @Override
+ public void onConnectionUpdated(String address, int interval, int latency,
+ int timeout, int status) {
+ if (DBG) {
+ Log.d(TAG, "onConnectionUpdated() - Device=" + address
+ + " interval=" + interval + " latency=" + latency
+ + " timeout=" + timeout + " status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onConnectionUpdated(BluetoothGatt.this, interval, latency,
+ timeout, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Callback invoked when service changed event is received
+ * @hide
+ */
+ @Override
+ public void onServiceChanged(String address) {
+ if (DBG) {
+ Log.d(TAG, "onServiceChanged() - Device=" + address);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onServiceChanged(BluetoothGatt.this);
+ }
+ }
+ });
+ }
+
+ /**
+ * Callback invoked when the given connection's subrate is changed
+ * @hide
+ */
+ @Override
+ public void onSubrateChange(String address, int subrateFactor, int latency,
+ int contNum, int timeout, int status) {
+ Log.d(TAG,
+ "onSubrateChange() - "
+ + "Device=" + BluetoothUtils.toAnonymizedAddress(address)
+ + ", subrateFactor=" + subrateFactor + ", latency=" + latency
+ + ", contNum=" + contNum + ", timeout=" + timeout
+ + ", status=" + status);
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onSubrateChange(BluetoothGatt.this, subrateFactor, latency,
+ contNum, timeout, status);
+ }
+ }
+ });
+ }
+ };
+
+ /* package */ BluetoothGatt(IBluetoothGatt iGatt, BluetoothDevice device, int transport,
+ boolean opportunistic, int phy, AttributionSource attributionSource) {
+ mService = iGatt;
+ mDevice = device;
+ mTransport = transport;
+ mPhy = phy;
+ mOpportunistic = opportunistic;
+ mAttributionSource = attributionSource;
+ mServices = new ArrayList<BluetoothGattService>();
+
+ mConnState = CONN_STATE_IDLE;
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+ }
+
+ /**
+ * Close this Bluetooth GATT client.
+ *
+ * <p>Application should call this method as early as possible after it is done with this GATT
+ * client.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @Override
+ public void close() {
+ if (DBG) Log.d(TAG, "close()");
+
+ unregisterApp();
+ mConnState = CONN_STATE_CLOSED;
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+ }
+
+ /**
+ * Returns a service by UUID, instance and type.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattService getService(BluetoothDevice device, UUID uuid,
+ int instanceId) {
+ for (BluetoothGattService svc : mServices) {
+ if (svc.getDevice().equals(device)
+ && svc.getInstanceId() == instanceId
+ && svc.getUuid().equals(uuid)) {
+ return svc;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Returns a characteristic with id equal to instanceId.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattCharacteristic getCharacteristicById(BluetoothDevice device,
+ int instanceId) {
+ for (BluetoothGattService svc : mServices) {
+ for (BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
+ if (charac.getInstanceId() == instanceId) {
+ return charac;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a descriptor with id equal to instanceId.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattDescriptor getDescriptorById(BluetoothDevice device, int instanceId) {
+ for (BluetoothGattService svc : mServices) {
+ for (BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
+ for (BluetoothGattDescriptor desc : charac.getDescriptors()) {
+ if (desc.getInstanceId() == instanceId) {
+ return desc;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Queue the runnable on a {@link Handler} provided by the user, or execute the runnable
+ * immediately if no Handler was provided.
+ */
+ private void runOrQueueCallback(final Runnable cb) {
+ if (mHandler == null) {
+ try {
+ cb.run();
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ } else {
+ mHandler.post(cb);
+ }
+ }
+
+ /**
+ * Register an application callback to start using GATT.
+ *
+ * <p>This is an asynchronous call. The callback {@link BluetoothGattCallback#onAppRegistered}
+ * is used to notify success or failure if the function returns true.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @return If true, the callback will be called to notify success or failure, false on immediate
+ * error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private boolean registerApp(BluetoothGattCallback callback, Handler handler) {
+ return registerApp(callback, handler, false);
+ }
+
+ /**
+ * Register an application callback to start using GATT.
+ *
+ * <p>This is an asynchronous call. The callback {@link BluetoothGattCallback#onAppRegistered}
+ * is used to notify success or failure if the function returns true.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param eattSupport indicate to allow for eatt support
+ * @return If true, the callback will be called to notify success or failure, false on immediate
+ * error
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private boolean registerApp(BluetoothGattCallback callback, Handler handler,
+ boolean eattSupport) {
+ if (DBG) Log.d(TAG, "registerApp()");
+ if (mService == null) return false;
+
+ mCallback = callback;
+ mHandler = handler;
+ UUID uuid = UUID.randomUUID();
+ if (DBG) Log.d(TAG, "registerApp() - UUID=" + uuid);
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.registerClient(new ParcelUuid(uuid), mBluetoothGattCallback, eattSupport,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Unregister the current application and callbacks.
+ */
+ @UnsupportedAppUsage
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private void unregisterApp() {
+ if (DBG) Log.d(TAG, "unregisterApp() - mClientIf=" + mClientIf);
+ if (mService == null || mClientIf == 0) return;
+
+ try {
+ mCallback = null;
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.unregisterClient(mClientIf, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ mClientIf = 0;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Initiate a connection to a Bluetooth GATT capable device.
+ *
+ * <p>The connection may not be established right away, but will be
+ * completed when the remote device is available. A
+ * {@link BluetoothGattCallback#onConnectionStateChange} callback will be
+ * invoked when the connection state changes as a result of this function.
+ *
+ * <p>The autoConnect parameter determines whether to actively connect to
+ * the remote device, or rather passively scan and finalize the connection
+ * when the remote device is in range/available. Generally, the first ever
+ * connection to a device should be direct (autoConnect set to false) and
+ * subsequent connections to known devices should be invoked with the
+ * autoConnect parameter set to true.
+ *
+ * @param device Remote device to connect to
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @return true, if the connection attempt was initiated successfully
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ /*package*/ boolean connect(Boolean autoConnect, BluetoothGattCallback callback,
+ Handler handler) {
+ if (DBG) {
+ Log.d(TAG,
+ "connect() - device: " + mDevice + ", auto: " + autoConnect);
+ }
+ synchronized (mStateLock) {
+ if (mConnState != CONN_STATE_IDLE) {
+ throw new IllegalStateException("Not idle");
+ }
+ mConnState = CONN_STATE_CONNECTING;
+ }
+
+ mAutoConnect = autoConnect;
+
+ if (!registerApp(callback, handler)) {
+ synchronized (mStateLock) {
+ mConnState = CONN_STATE_IDLE;
+ }
+ Log.e(TAG, "Failed to register callback");
+ return false;
+ }
+
+ // The connection will continue in the onClientRegistered callback
+ return true;
+ }
+
+ /**
+ * Disconnects an established connection, or cancels a connection attempt
+ * currently in progress.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void disconnect() {
+ if (DBG) Log.d(TAG, "cancelOpen() - device: " + mDevice);
+ if (mService == null || mClientIf == 0) return;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.clientDisconnect(mClientIf, mDevice.getAddress(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Connect back to remote device.
+ *
+ * <p>This method is used to re-connect to a remote device after the
+ * connection has been dropped. If the device is not in range, the
+ * re-connection will be triggered once the device is back in range.
+ *
+ * @return true, if the connection attempt was initiated successfully
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connect() {
+ try {
+ if (DBG) {
+ Log.d(TAG, "connect(void) - device: " + mDevice
+ + ", auto=" + mAutoConnect);
+ }
+
+ // autoConnect is inverse of "isDirect"
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.clientConnect(mClientIf, mDevice.getAddress(), mDevice.getAddressType(),
+ !mAutoConnect, mTransport, mOpportunistic, mPhy, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ return true;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ }
+
+ /**
+ * Set the preferred connection PHY for this app. Please note that this is just a
+ * recommendation, whether the PHY change will happen depends on other applications preferences,
+ * local and remote controller capabilities. Controller can override these settings.
+ * <p>
+ * {@link BluetoothGattCallback#onPhyUpdate} will be triggered as a result of this call, even
+ * if no PHY change happens. It is also triggered when remote device updates the PHY.
+ *
+ * @param txPhy preferred transmitter PHY. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}.
+ * @param rxPhy preferred receiver PHY. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}.
+ * @param phyOptions preferred coding to use when transmitting on the LE Coded PHY. Can be one
+ * of {@link BluetoothDevice#PHY_OPTION_NO_PREFERRED}, {@link BluetoothDevice#PHY_OPTION_S2} or
+ * {@link BluetoothDevice#PHY_OPTION_S8}
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setPreferredPhy(int txPhy, int rxPhy, int phyOptions) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.clientSetPreferredPhy(mClientIf, mDevice.getAddress(), txPhy, rxPhy,
+ phyOptions, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Read the current transmitter PHY and receiver PHY of the connection. The values are returned
+ * in {@link BluetoothGattCallback#onPhyRead}
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void readPhy() {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.clientReadPhy(mClientIf, mDevice.getAddress(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Return the remote bluetooth device this GATT client targets to
+ *
+ * @return remote bluetooth device
+ */
+ @RequiresNoPermission
+ public BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Discovers services offered by a remote device as well as their
+ * characteristics and descriptors.
+ *
+ * <p>This is an asynchronous operation. Once service discovery is completed,
+ * the {@link BluetoothGattCallback#onServicesDiscovered} callback is
+ * triggered. If the discovery was successful, the remote services can be
+ * retrieved using the {@link #getServices} function.
+ *
+ * @return true, if the remote service discovery has been started
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean discoverServices() {
+ if (DBG) Log.d(TAG, "discoverServices() - device: " + mDevice);
+ if (mService == null || mClientIf == 0) return false;
+
+ mServices.clear();
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.discoverServices(mClientIf, mDevice.getAddress(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Discovers a service by UUID. This is exposed only for passing PTS tests.
+ * It should never be used by real applications. The service is not searched
+ * for characteristics and descriptors, or returned in any callback.
+ *
+ * @return true, if the remote service discovery has been started
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean discoverServiceByUuid(UUID uuid) {
+ if (DBG) Log.d(TAG, "discoverServiceByUuid() - device: " + mDevice);
+ if (mService == null || mClientIf == 0) return false;
+
+ mServices.clear();
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.discoverServiceByUuid(mClientIf, mDevice.getAddress(), new ParcelUuid(uuid),
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns a list of GATT services offered by the remote device.
+ *
+ * <p>This function requires that service discovery has been completed
+ * for the given device.
+ *
+ * @return List of services on the remote device. Returns an empty list if service discovery has
+ * not yet been performed.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public List<BluetoothGattService> getServices() {
+ List<BluetoothGattService> result =
+ new ArrayList<BluetoothGattService>();
+
+ for (BluetoothGattService service : mServices) {
+ if (service.getDevice().equals(mDevice)) {
+ result.add(service);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns a {@link BluetoothGattService}, if the requested UUID is
+ * supported by the remote device.
+ *
+ * <p>This function requires that service discovery has been completed
+ * for the given device.
+ *
+ * <p>If multiple instances of the same service (as identified by UUID)
+ * exist, the first instance of the service is returned.
+ *
+ * @param uuid UUID of the requested service
+ * @return BluetoothGattService if supported, or null if the requested service is not offered by
+ * the remote device.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public BluetoothGattService getService(UUID uuid) {
+ for (BluetoothGattService service : mServices) {
+ if (service.getDevice().equals(mDevice) && service.getUuid().equals(uuid)) {
+ return service;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Reads the requested characteristic from the associated remote device.
+ *
+ * <p>This is an asynchronous operation. The result of the read operation
+ * is reported by the {@link BluetoothGattCallback#onCharacteristicRead(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[], int)} callback.
+ *
+ * @param characteristic Characteristic to read from the remote device
+ * @return true, if the read operation was initiated successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
+ if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0) {
+ return false;
+ }
+
+ if (VDBG) Log.d(TAG, "readCharacteristic() - uuid: " + characteristic.getUuid());
+ if (mService == null || mClientIf == 0) return false;
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) return false;
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) return false;
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return false;
+ mDeviceBusy = true;
+ }
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.readCharacteristic(mClientIf, device.getAddress(),
+ characteristic.getInstanceId(), AUTHENTICATION_NONE, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Reads the characteristic using its UUID from the associated remote device.
+ *
+ * <p>This is an asynchronous operation. The result of the read operation
+ * is reported by the {@link BluetoothGattCallback#onCharacteristicRead(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[], int)} callback.
+ *
+ * @param uuid UUID of characteristic to read from the remote device
+ * @return true, if the read operation was initiated successfully
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean readUsingCharacteristicUuid(UUID uuid, int startHandle, int endHandle) {
+ if (VDBG) Log.d(TAG, "readUsingCharacteristicUuid() - uuid: " + uuid);
+ if (mService == null || mClientIf == 0) return false;
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return false;
+ mDeviceBusy = true;
+ }
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.readUsingCharacteristicUuid(mClientIf, mDevice.getAddress(),
+ new ParcelUuid(uuid), startHandle, endHandle, AUTHENTICATION_NONE,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Writes a given characteristic and its values to the associated remote device.
+ *
+ * <p>Once the write operation has been completed, the
+ * {@link BluetoothGattCallback#onCharacteristicWrite} callback is invoked,
+ * reporting the result of the operation.
+ *
+ * @param characteristic Characteristic to write on the remote device
+ * @return true, if the write operation was initiated successfully
+ * @throws IllegalArgumentException if characteristic or its value are null
+ *
+ * @deprecated Use {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[],
+ * int)} as this is not memory safe because it relies on a {@link BluetoothGattCharacteristic}
+ * object whose underlying fields are subject to change outside this method.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic) {
+ try {
+ return writeCharacteristic(characteristic, characteristic.getValue(),
+ characteristic.getWriteType()) == BluetoothStatusCodes.SUCCESS;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY,
+ BluetoothStatusCodes.ERROR_UNKNOWN
+ })
+ public @interface WriteOperationReturnValues{}
+
+ /**
+ * Writes a given characteristic and its values to the associated remote device.
+ *
+ * <p>Once the write operation has been completed, the
+ * {@link BluetoothGattCallback#onCharacteristicWrite} callback is invoked,
+ * reporting the result of the operation.
+ *
+ * @param characteristic Characteristic to write on the remote device
+ * @return whether the characteristic was successfully written to
+ * @throws IllegalArgumentException if characteristic or value are null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @WriteOperationReturnValues
+ public int writeCharacteristic(@NonNull BluetoothGattCharacteristic characteristic,
+ @NonNull byte[] value, @WriteType int writeType) {
+ if (characteristic == null) {
+ throw new IllegalArgumentException("characteristic must not be null");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("value must not be null");
+ }
+ if (VDBG) Log.d(TAG, "writeCharacteristic() - uuid: " + characteristic.getUuid());
+ if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0
+ && (characteristic.getProperties()
+ & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) {
+ return BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED;
+ }
+ if (mService == null || mClientIf == 0) {
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ }
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) {
+ throw new IllegalArgumentException("Characteristic must have a non-null service");
+ }
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) {
+ throw new IllegalArgumentException("Service must have a non-null device");
+ }
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) {
+ return BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY;
+ }
+ mDeviceBusy = true;
+ }
+
+ int requestStatus = BluetoothStatusCodes.ERROR_UNKNOWN;
+ try {
+ for (int i = 0; i < WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.writeCharacteristic(mClientIf, device.getAddress(),
+ characteristic.getInstanceId(), writeType, AUTHENTICATION_NONE, value,
+ mAttributionSource, recv);
+ requestStatus = recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND);
+ if (requestStatus != BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY) {
+ break;
+ }
+ try {
+ Thread.sleep(WRITE_CHARACTERISTIC_TIME_TO_WAIT);
+ } catch (InterruptedException e) {
+ }
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ throw e.rethrowFromSystemServer();
+ }
+
+ return requestStatus;
+ }
+
+ /**
+ * Reads the value for a given descriptor from the associated remote device.
+ *
+ * <p>Once the read operation has been completed, the
+ * {@link BluetoothGattCallback#onDescriptorRead} callback is
+ * triggered, signaling the result of the operation.
+ *
+ * @param descriptor Descriptor value to read from the remote device
+ * @return true, if the read operation was initiated successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean readDescriptor(BluetoothGattDescriptor descriptor) {
+ if (VDBG) Log.d(TAG, "readDescriptor() - uuid: " + descriptor.getUuid());
+ if (mService == null || mClientIf == 0) return false;
+
+ BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
+ if (characteristic == null) return false;
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) return false;
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) return false;
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return false;
+ mDeviceBusy = true;
+ }
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.readDescriptor(mClientIf, device.getAddress(),
+ descriptor.getInstanceId(), AUTHENTICATION_NONE, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Write the value of a given descriptor to the associated remote device.
+ *
+ * <p>A {@link BluetoothGattCallback#onDescriptorWrite} callback is triggered to report the
+ * result of the write operation.
+ *
+ * @param descriptor Descriptor to write to the associated remote device
+ * @return true, if the write operation was initiated successfully
+ * @throws IllegalArgumentException if descriptor or its value are null
+ *
+ * @deprecated Use {@link BluetoothGatt#writeDescriptor(BluetoothGattDescriptor, byte[])} as
+ * this is not memory safe because it relies on a {@link BluetoothGattDescriptor} object
+ * whose underlying fields are subject to change outside this method.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean writeDescriptor(BluetoothGattDescriptor descriptor) {
+ try {
+ return writeDescriptor(descriptor, descriptor.getValue())
+ == BluetoothStatusCodes.SUCCESS;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Write the value of a given descriptor to the associated remote device.
+ *
+ * <p>A {@link BluetoothGattCallback#onDescriptorWrite} callback is triggered to report the
+ * result of the write operation.
+ *
+ * @param descriptor Descriptor to write to the associated remote device
+ * @return true, if the write operation was initiated successfully
+ * @throws IllegalArgumentException if descriptor or value are null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @WriteOperationReturnValues
+ public int writeDescriptor(@NonNull BluetoothGattDescriptor descriptor,
+ @NonNull byte[] value) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("value must not be null");
+ }
+ if (VDBG) Log.d(TAG, "writeDescriptor() - uuid: " + descriptor.getUuid());
+ if (mService == null || mClientIf == 0) {
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ }
+
+ BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
+ if (characteristic == null) {
+ throw new IllegalArgumentException("Descriptor must have a non-null characteristic");
+ }
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) {
+ throw new IllegalArgumentException("Characteristic must have a non-null service");
+ }
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) {
+ throw new IllegalArgumentException("Service must have a non-null device");
+ }
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY;
+ mDeviceBusy = true;
+ }
+
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.writeDescriptor(mClientIf, device.getAddress(),
+ descriptor.getInstanceId(), AUTHENTICATION_NONE, value, mAttributionSource,
+ recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND);
+ } catch (TimeoutException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ e.rethrowFromSystemServer();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Initiates a reliable write transaction for a given remote device.
+ *
+ * <p>Once a reliable write transaction has been initiated, all calls
+ * to {@link #writeCharacteristic} are sent to the remote device for
+ * verification and queued up for atomic execution. The application will
+ * receive a {@link BluetoothGattCallback#onCharacteristicWrite} callback in response to every
+ * {@link #writeCharacteristic(BluetoothGattCharacteristic, byte[], int)} call and is
+ * responsible for verifying if the value has been transmitted accurately.
+ *
+ * <p>After all characteristics have been queued up and verified,
+ * {@link #executeReliableWrite} will execute all writes. If a characteristic
+ * was not written correctly, calling {@link #abortReliableWrite} will
+ * cancel the current transaction without committing any values on the
+ * remote device.
+ *
+ * @return true, if the reliable write transaction has been initiated
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean beginReliableWrite() {
+ if (VDBG) Log.d(TAG, "beginReliableWrite() - device: " + mDevice);
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.beginReliableWrite(mClientIf, mDevice.getAddress(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Executes a reliable write transaction for a given remote device.
+ *
+ * <p>This function will commit all queued up characteristic write
+ * operations for a given remote device.
+ *
+ * <p>A {@link BluetoothGattCallback#onReliableWriteCompleted} callback is
+ * invoked to indicate whether the transaction has been executed correctly.
+ *
+ * @return true, if the request to execute the transaction has been sent
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean executeReliableWrite() {
+ if (VDBG) Log.d(TAG, "executeReliableWrite() - device: " + mDevice);
+ if (mService == null || mClientIf == 0) return false;
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return false;
+ mDeviceBusy = true;
+ }
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.endReliableWrite(mClientIf, mDevice.getAddress(), true, mAttributionSource,
+ recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Cancels a reliable write transaction for a given device.
+ *
+ * <p>Calling this function will discard all queued characteristic write
+ * operations for a given remote device.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void abortReliableWrite() {
+ if (VDBG) Log.d(TAG, "abortReliableWrite() - device: " + mDevice);
+ if (mService == null || mClientIf == 0) return;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.endReliableWrite(mClientIf, mDevice.getAddress(), false, mAttributionSource,
+ recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #abortReliableWrite()}
+ */
+ @Deprecated
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void abortReliableWrite(BluetoothDevice mDevice) {
+ abortReliableWrite();
+ }
+
+ /**
+ * Enable or disable notifications/indications for a given characteristic.
+ *
+ * <p>Once notifications are enabled for a characteristic, a
+ * {@link BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[])} callback will be triggered if the remote device
+ * indicates that the given characteristic has changed.
+ *
+ * @param characteristic The characteristic for which to enable notifications
+ * @param enable Set to true to enable notifications/indications
+ * @return true, if the requested notification status was set successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
+ boolean enable) {
+ if (DBG) {
+ Log.d(TAG, "setCharacteristicNotification() - uuid: " + characteristic.getUuid()
+ + " enable: " + enable);
+ }
+ if (mService == null || mClientIf == 0) return false;
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) return false;
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.registerForNotification(mClientIf, device.getAddress(),
+ characteristic.getInstanceId(), enable, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Clears the internal cache and forces a refresh of the services from the
+ * remote device.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean refresh() {
+ if (DBG) Log.d(TAG, "refresh() - device: " + mDevice);
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.refreshDevice(mClientIf, mDevice.getAddress(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Read the RSSI for a connected remote device.
+ *
+ * <p>The {@link BluetoothGattCallback#onReadRemoteRssi} callback will be
+ * invoked when the RSSI value has been read.
+ *
+ * @return true, if the RSSI value has been requested successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean readRemoteRssi() {
+ if (DBG) Log.d(TAG, "readRssi() - device: " + mDevice);
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.readRemoteRssi(mClientIf, mDevice.getAddress(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Request an MTU size used for a given connection.
+ *
+ * <p>When performing a write request operation (write without response),
+ * the data sent is truncated to the MTU size. This function may be used
+ * to request a larger MTU size to be able to send more data at once.
+ *
+ * <p>A {@link BluetoothGattCallback#onMtuChanged} callback will indicate
+ * whether this operation was successful.
+ *
+ * @return true, if the new MTU value has been requested successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean requestMtu(int mtu) {
+ if (DBG) {
+ Log.d(TAG, "configureMTU() - device: " + mDevice
+ + " mtu: " + mtu);
+ }
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.configureMTU(mClientIf, mDevice.getAddress(), mtu, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Request a connection parameter update.
+ *
+ * <p>This function will send a connection parameter update request to the
+ * remote device.
+ *
+ * @param connectionPriority Request a specific connection priority. Must be one of {@link
+ * BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}
+ * {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}, or
+ * {@link BluetoothGatt#CONNECTION_PRIORITY_DCK}.
+ * @throws IllegalArgumentException If the parameters are outside of their specified range.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean requestConnectionPriority(int connectionPriority) {
+ if (connectionPriority < CONNECTION_PRIORITY_BALANCED
+ || connectionPriority > CONNECTION_PRIORITY_DCK) {
+ throw new IllegalArgumentException("connectionPriority not within valid range");
+ }
+
+ if (DBG) Log.d(TAG, "requestConnectionPriority() - params: " + connectionPriority);
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.connectionParameterUpdate(mClientIf, mDevice.getAddress(), connectionPriority,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Request an LE connection parameter update.
+ *
+ * <p>This function will send an LE connection parameters update request to the remote device.
+ *
+ * @return true, if the request is send to the Bluetooth stack.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean requestLeConnectionUpdate(int minConnectionInterval, int maxConnectionInterval,
+ int slaveLatency, int supervisionTimeout,
+ int minConnectionEventLen, int maxConnectionEventLen) {
+ if (DBG) {
+ Log.d(TAG, "requestLeConnectionUpdate() - min=(" + minConnectionInterval
+ + ")" + (1.25 * minConnectionInterval)
+ + "msec, max=(" + maxConnectionInterval + ")"
+ + (1.25 * maxConnectionInterval) + "msec, latency=" + slaveLatency
+ + ", timeout=" + supervisionTimeout + "msec" + ", min_ce="
+ + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen);
+ }
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.leConnectionUpdate(mClientIf, mDevice.getAddress(),
+ minConnectionInterval, maxConnectionInterval,
+ slaveLatency, supervisionTimeout,
+ minConnectionEventLen, maxConnectionEventLen,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Request LE subrate mode.
+ *
+ * <p>This function will send a LE subrate request to the remote device.
+ *
+ * @param subrateMode Request a specific subrate mode.
+ * @throws IllegalArgumentException If the parameters are outside of their specified range.
+ * @return true, if the request is send to the Bluetooth stack.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean requestSubrateMode(@SubrateRequestMode int subrateMode) {
+ if (subrateMode < SUBRATE_REQUEST_MODE_BALANCED
+ || subrateMode > SUBRATE_REQUEST_MODE_LOW_POWER) {
+ throw new IllegalArgumentException("Subrate Mode not within valid range");
+ }
+
+ if (DBG) {
+ Log.d(TAG, "requestsubrateMode() - subrateMode: " + subrateMode);
+ }
+ if (mService == null || mClientIf == 0) {
+ return false;
+ }
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.subrateModeRequest(
+ mClientIf, mDevice.getAddress(), subrateMode, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Request a LE subrate request.
+ *
+ * <p>This function will send a LE subrate request to the remote device.
+ *
+ * @return true, if the request is send to the Bluetooth stack.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean bleSubrateRequest(int subrateMin, int subrateMax, int maxLatency, int contNumber,
+ int supervisionTimeout) {
+ if (DBG) {
+ Log.d(TAG,
+ "bleSubrateRequest() - subrateMin=" + subrateMin + " subrateMax=" + (subrateMax)
+ + " maxLatency= " + maxLatency + "contNumber=" + contNumber
+ + " supervisionTimeout=" + supervisionTimeout);
+ }
+ if (mService == null || mClientIf == 0) {
+ return false;
+ }
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.leSubrateRequest(mClientIf, mDevice.getAddress(), subrateMin, subrateMax,
+ maxLatency, contNumber, supervisionTimeout, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @deprecated Not supported - please use {@link BluetoothManager#getConnectedDevices(int)}
+ * with {@link BluetoothProfile#GATT} as argument
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ @Deprecated
+ public int getConnectionState(BluetoothDevice device) {
+ throw new UnsupportedOperationException("Use BluetoothManager#getConnectionState instead.");
+ }
+
+ /**
+ * @deprecated Not supported - please use {@link BluetoothManager#getConnectedDevices(int)}
+ * with {@link BluetoothProfile#GATT} as argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ @Deprecated
+ public List<BluetoothDevice> getConnectedDevices() {
+ throw new UnsupportedOperationException(
+ "Use BluetoothManager#getConnectedDevices instead.");
+ }
+
+ /**
+ * @deprecated Not supported - please use
+ * {@link BluetoothManager#getDevicesMatchingConnectionStates(int, int[])}
+ * with {@link BluetoothProfile#GATT} as first argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ @Deprecated
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ throw new UnsupportedOperationException(
+ "Use BluetoothManager#getDevicesMatchingConnectionStates instead.");
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothGattCallback.java b/android-34/android/bluetooth/BluetoothGattCallback.java
new file mode 100644
index 0000000..fa5ab50
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothGattCallback.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+
+/**
+ * This abstract class is used to implement {@link BluetoothGatt} callbacks.
+ */
+public abstract class BluetoothGattCallback {
+
+ /**
+ * Callback triggered as result of {@link BluetoothGatt#setPreferredPhy}, or as a result of
+ * remote device changing the PHY.
+ *
+ * @param gatt GATT client
+ * @param txPhy the transmitter PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}.
+ * @param rxPhy the receiver PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}.
+ * @param status Status of the PHY update operation. {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
+ }
+
+ /**
+ * Callback triggered as result of {@link BluetoothGatt#readPhy}
+ *
+ * @param gatt GATT client
+ * @param txPhy the transmitter PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}.
+ * @param rxPhy the receiver PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}.
+ * @param status Status of the PHY read operation. {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
+ }
+
+ /**
+ * Callback indicating when GATT client has connected/disconnected to/from a remote
+ * GATT server.
+ *
+ * @param gatt GATT client
+ * @param status Status of the connect or disconnect operation. {@link
+ * BluetoothGatt#GATT_SUCCESS} if the operation succeeds.
+ * @param newState Returns the new connection state. Can be one of {@link
+ * BluetoothProfile#STATE_DISCONNECTED} or {@link BluetoothProfile#STATE_CONNECTED}
+ */
+ public void onConnectionStateChange(BluetoothGatt gatt, int status,
+ int newState) {
+ }
+
+ /**
+ * Callback invoked when the list of remote services, characteristics and descriptors
+ * for the remote device have been updated, ie new services have been discovered.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#discoverServices}
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the remote device has been explored
+ * successfully.
+ */
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ }
+
+ /**
+ * Callback reporting the result of a characteristic read operation.
+ *
+ * @param gatt GATT client invoked
+ * {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)}
+ * @param characteristic Characteristic that was read from the associated remote device.
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the read operation was completed
+ * successfully.
+ * @deprecated Use {@link BluetoothGattCallback#onCharacteristicRead(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[], int)} as it is memory safe
+ */
+ @Deprecated
+ public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
+ int status) {
+ }
+
+ /**
+ * Callback reporting the result of a characteristic read operation.
+ *
+ * @param gatt GATT client invoked
+ * {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)}
+ * @param characteristic Characteristic that was read from the associated remote device.
+ * @param value the value of the characteristic
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the read operation was completed
+ * successfully.
+ */
+ public void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull
+ BluetoothGattCharacteristic characteristic, @NonNull byte[] value, int status) {
+ onCharacteristicRead(gatt, characteristic, status);
+ }
+
+ /**
+ * Callback indicating the result of a characteristic write operation.
+ *
+ * <p>If this callback is invoked while a reliable write transaction is
+ * in progress, the value of the characteristic represents the value
+ * reported by the remote device. An application should compare this
+ * value to the desired value to be written. If the values don't match,
+ * the application must abort the reliable write transaction.
+ *
+ * @param gatt GATT client that invoked
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic,
+ * byte[], int)}
+ * @param characteristic Characteristic that was written to the associated remote device.
+ * @param status The result of the write operation {@link BluetoothGatt#GATT_SUCCESS} if
+ * the
+ * operation succeeds.
+ */
+ public void onCharacteristicWrite(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ }
+
+ /**
+ * Callback triggered as a result of a remote characteristic notification.
+ *
+ * @param gatt GATT client the characteristic is associated with
+ * @param characteristic Characteristic that has been updated as a result of a remote
+ * notification event.
+ * @deprecated Use {@link BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[])} as it is memory safe by providing the characteristic
+ * value at the time of notification.
+ */
+ @Deprecated
+ public void onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ }
+
+ /**
+ * Callback triggered as a result of a remote characteristic notification. Note that the value
+ * within the characteristic object may have changed since receiving the remote characteristic
+ * notification, so check the parameter value for the value at the time of notification.
+ *
+ * @param gatt GATT client the characteristic is associated with
+ * @param characteristic Characteristic that has been updated as a result of a remote
+ * notification event.
+ * @param value notified characteristic value
+ */
+ public void onCharacteristicChanged(@NonNull BluetoothGatt gatt,
+ @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
+ onCharacteristicChanged(gatt, characteristic);
+ }
+
+ /**
+ * Callback reporting the result of a descriptor read operation.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#readDescriptor}
+ * @param descriptor Descriptor that was read from the associated remote device.
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the read operation was completed
+ * successfully
+ * @deprecated Use {@link BluetoothGattCallback#onDescriptorRead(BluetoothGatt,
+ * BluetoothGattDescriptor, int, byte[])} as it is memory safe by providing the descriptor
+ * value at the time it was read.
+ */
+ @Deprecated
+ public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+ int status) {
+ }
+
+ /**
+ * Callback reporting the result of a descriptor read operation.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#readDescriptor}
+ * @param descriptor Descriptor that was read from the associated remote device.
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the read operation was completed
+ * successfully
+ * @param value the descriptor value at the time of the read operation
+ */
+ public void onDescriptorRead(@NonNull BluetoothGatt gatt,
+ @NonNull BluetoothGattDescriptor descriptor, int status, @NonNull byte[] value) {
+ onDescriptorRead(gatt, descriptor, status);
+ }
+
+ /**
+ * Callback indicating the result of a descriptor write operation.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#writeDescriptor}
+ * @param descriptor Descriptor that was writte to the associated remote device.
+ * @param status The result of the write operation {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+ int status) {
+ }
+
+ /**
+ * Callback invoked when a reliable write transaction has been completed.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#executeReliableWrite}
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the reliable write transaction was
+ * executed successfully
+ */
+ public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+ }
+
+ /**
+ * Callback reporting the RSSI for a remote device connection.
+ *
+ * This callback is triggered in response to the
+ * {@link BluetoothGatt#readRemoteRssi} function.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#readRemoteRssi}
+ * @param rssi The RSSI value for the remote device
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the RSSI was read successfully
+ */
+ public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+ }
+
+ /**
+ * Callback indicating the MTU for a given device connection has changed.
+ *
+ * This callback is triggered in response to the
+ * {@link BluetoothGatt#requestMtu} function, or in response to a connection
+ * event.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#requestMtu}
+ * @param mtu The new MTU size
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the MTU has been changed successfully
+ */
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+ }
+
+ /**
+ * Callback indicating the connection parameters were updated.
+ *
+ * @param gatt GATT client involved
+ * @param interval Connection interval used on this connection, 1.25ms unit. Valid range is from
+ * 6 (7.5ms) to 3200 (4000ms).
+ * @param latency Worker latency for the connection in number of connection events. Valid range
+ * is from 0 to 499
+ * @param timeout Supervision timeout for this connection, in 10ms unit. Valid range is from 10
+ * (0.1s) to 3200 (32s)
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the connection has been updated
+ * successfully
+ * @hide
+ */
+ public void onConnectionUpdated(BluetoothGatt gatt, int interval, int latency, int timeout,
+ int status) {
+ }
+
+ /**
+ * Callback indicating service changed event is received
+ *
+ * <p>Receiving this event means that the GATT database is out of sync with
+ * the remote device. {@link BluetoothGatt#discoverServices} should be
+ * called to re-discover the services.
+ *
+ * @param gatt GATT client involved
+ */
+ public void onServiceChanged(@NonNull BluetoothGatt gatt) {
+ }
+
+ /**
+ * Callback indicating LE connection's subrate parameters have changed.
+ *
+ * @param gatt GATT client involved
+ * @param subrateFactor for the LE connection.
+ * @param latency Worker latency for the connection in number of connection events. Valid range
+ * is from 0 to 499
+ * @param contNum Valid range is from 0 to 499.
+ * @param timeout Supervision timeout for this connection, in 10ms unit. Valid range is from 10
+ * (0.1s) to 3200 (32s)
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if LE connection subrating has been changed
+ * successfully.
+ * @hide
+ */
+ public void onSubrateChange(BluetoothGatt gatt, int subrateFactor, int latency, int contNum,
+ int timeout, int status) {}
+}
diff --git a/android-34/android/bluetooth/BluetoothGattCharacteristic.java b/android-34/android/bluetooth/BluetoothGattCharacteristic.java
new file mode 100644
index 0000000..88f9517
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothGattCharacteristic.java
@@ -0,0 +1,818 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.bluetooth;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a Bluetooth GATT Characteristic
+ *
+ * <p>A GATT characteristic is a basic data element used to construct a GATT service,
+ * {@link BluetoothGattService}. The characteristic contains a value as well as
+ * additional information and optional GATT descriptors, {@link BluetoothGattDescriptor}.
+ */
+public class BluetoothGattCharacteristic implements Parcelable {
+
+ /**
+ * Characteristic proprty: Characteristic is broadcastable.
+ */
+ public static final int PROPERTY_BROADCAST = 0x01;
+
+ /**
+ * Characteristic property: Characteristic is readable.
+ */
+ public static final int PROPERTY_READ = 0x02;
+
+ /**
+ * Characteristic property: Characteristic can be written without response.
+ */
+ public static final int PROPERTY_WRITE_NO_RESPONSE = 0x04;
+
+ /**
+ * Characteristic property: Characteristic can be written.
+ */
+ public static final int PROPERTY_WRITE = 0x08;
+
+ /**
+ * Characteristic property: Characteristic supports notification
+ */
+ public static final int PROPERTY_NOTIFY = 0x10;
+
+ /**
+ * Characteristic property: Characteristic supports indication
+ */
+ public static final int PROPERTY_INDICATE = 0x20;
+
+ /**
+ * Characteristic property: Characteristic supports write with signature
+ */
+ public static final int PROPERTY_SIGNED_WRITE = 0x40;
+
+ /**
+ * Characteristic property: Characteristic has extended properties
+ */
+ public static final int PROPERTY_EXTENDED_PROPS = 0x80;
+
+ /**
+ * Characteristic read permission
+ */
+ public static final int PERMISSION_READ = 0x01;
+
+ /**
+ * Characteristic permission: Allow encrypted read operations
+ */
+ public static final int PERMISSION_READ_ENCRYPTED = 0x02;
+
+ /**
+ * Characteristic permission: Allow reading with person-in-the-middle protection
+ */
+ public static final int PERMISSION_READ_ENCRYPTED_MITM = 0x04;
+
+ /**
+ * Characteristic write permission
+ */
+ public static final int PERMISSION_WRITE = 0x10;
+
+ /**
+ * Characteristic permission: Allow encrypted writes
+ */
+ public static final int PERMISSION_WRITE_ENCRYPTED = 0x20;
+
+ /**
+ * Characteristic permission: Allow encrypted writes with person-in-the-middle
+ * protection
+ */
+ public static final int PERMISSION_WRITE_ENCRYPTED_MITM = 0x40;
+
+ /**
+ * Characteristic permission: Allow signed write operations
+ */
+ public static final int PERMISSION_WRITE_SIGNED = 0x80;
+
+ /**
+ * Characteristic permission: Allow signed write operations with
+ * person-in-the-middle protection
+ */
+ public static final int PERMISSION_WRITE_SIGNED_MITM = 0x100;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "WRITE_TYPE_", value = {
+ WRITE_TYPE_DEFAULT,
+ WRITE_TYPE_NO_RESPONSE,
+ WRITE_TYPE_SIGNED
+ })
+ public @interface WriteType{}
+
+ /**
+ * Write characteristic, requesting acknowledgement by the remote device
+ */
+ public static final int WRITE_TYPE_DEFAULT = 0x02;
+
+ /**
+ * Write characteristic without requiring a response by the remote device
+ */
+ public static final int WRITE_TYPE_NO_RESPONSE = 0x01;
+
+ /**
+ * Write characteristic including authentication signature
+ */
+ public static final int WRITE_TYPE_SIGNED = 0x04;
+
+ /**
+ * Characteristic value format type uint8
+ */
+ public static final int FORMAT_UINT8 = 0x11;
+
+ /**
+ * Characteristic value format type uint16
+ */
+ public static final int FORMAT_UINT16 = 0x12;
+
+ /**
+ * Characteristic value format type uint32
+ */
+ public static final int FORMAT_UINT32 = 0x14;
+
+ /**
+ * Characteristic value format type sint8
+ */
+ public static final int FORMAT_SINT8 = 0x21;
+
+ /**
+ * Characteristic value format type sint16
+ */
+ public static final int FORMAT_SINT16 = 0x22;
+
+ /**
+ * Characteristic value format type sint32
+ */
+ public static final int FORMAT_SINT32 = 0x24;
+
+ /**
+ * Characteristic value format type sfloat (16-bit float)
+ */
+ public static final int FORMAT_SFLOAT = 0x32;
+
+ /**
+ * Characteristic value format type float (32-bit float)
+ */
+ public static final int FORMAT_FLOAT = 0x34;
+
+
+ /**
+ * The UUID of this characteristic.
+ *
+ * @hide
+ */
+ protected UUID mUuid;
+
+ /**
+ * Instance ID for this characteristic.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected int mInstance;
+
+ /**
+ * Characteristic properties.
+ *
+ * @hide
+ */
+ protected int mProperties;
+
+ /**
+ * Characteristic permissions.
+ *
+ * @hide
+ */
+ protected int mPermissions;
+
+ /**
+ * Key size (default = 16).
+ *
+ * @hide
+ */
+ protected int mKeySize = 16;
+
+ /**
+ * Write type for this characteristic.
+ * See WRITE_TYPE_* constants.
+ *
+ * @hide
+ */
+ protected int mWriteType;
+
+ /**
+ * Back-reference to the service this characteristic belongs to.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected BluetoothGattService mService;
+
+ /**
+ * The cached value of this characteristic.
+ *
+ * @hide
+ */
+ protected byte[] mValue;
+
+ /**
+ * List of descriptors included in this characteristic.
+ */
+ protected List<BluetoothGattDescriptor> mDescriptors;
+
+ /**
+ * Create a new BluetoothGattCharacteristic.
+ *
+ * @param uuid The UUID for this characteristic
+ * @param properties Properties of this characteristic
+ * @param permissions Permissions for this characteristic
+ */
+ public BluetoothGattCharacteristic(UUID uuid, int properties, int permissions) {
+ initCharacteristic(null, uuid, 0, properties, permissions);
+ }
+
+ /**
+ * Create a new BluetoothGattCharacteristic
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattCharacteristic(BluetoothGattService service,
+ UUID uuid, int instanceId,
+ int properties, int permissions) {
+ initCharacteristic(service, uuid, instanceId, properties, permissions);
+ }
+
+ /**
+ * Create a new BluetoothGattCharacteristic
+ *
+ * @hide
+ */
+ public BluetoothGattCharacteristic(UUID uuid, int instanceId,
+ int properties, int permissions) {
+ initCharacteristic(null, uuid, instanceId, properties, permissions);
+ }
+
+ private void initCharacteristic(BluetoothGattService service,
+ UUID uuid, int instanceId,
+ int properties, int permissions) {
+ mUuid = uuid;
+ mInstance = instanceId;
+ mProperties = properties;
+ mPermissions = permissions;
+ mService = service;
+ mValue = null;
+ mDescriptors = new ArrayList<BluetoothGattDescriptor>();
+
+ if ((mProperties & PROPERTY_WRITE_NO_RESPONSE) != 0) {
+ mWriteType = WRITE_TYPE_NO_RESPONSE;
+ } else {
+ mWriteType = WRITE_TYPE_DEFAULT;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeInt(mInstance);
+ out.writeInt(mProperties);
+ out.writeInt(mPermissions);
+ out.writeInt(mKeySize);
+ out.writeInt(mWriteType);
+ out.writeTypedList(mDescriptors);
+ }
+
+ public static final @NonNull Creator<BluetoothGattCharacteristic> CREATOR = new Creator<>() {
+ public BluetoothGattCharacteristic createFromParcel(Parcel in) {
+ return new BluetoothGattCharacteristic(in);
+ }
+
+ public BluetoothGattCharacteristic[] newArray(int size) {
+ return new BluetoothGattCharacteristic[size];
+ }
+ };
+
+ private BluetoothGattCharacteristic(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mInstance = in.readInt();
+ mProperties = in.readInt();
+ mPermissions = in.readInt();
+ mKeySize = in.readInt();
+ mWriteType = in.readInt();
+
+ mDescriptors = new ArrayList<BluetoothGattDescriptor>();
+
+ ArrayList<BluetoothGattDescriptor> descs =
+ in.createTypedArrayList(BluetoothGattDescriptor.CREATOR);
+ if (descs != null) {
+ for (BluetoothGattDescriptor desc : descs) {
+ desc.setCharacteristic(this);
+ mDescriptors.add(desc);
+ }
+ }
+ }
+
+ /**
+ * Returns the desired key size.
+ *
+ * @hide
+ */
+ public int getKeySize() {
+ return mKeySize;
+ }
+
+ /**
+ * Adds a descriptor to this characteristic.
+ *
+ * @param descriptor Descriptor to be added to this characteristic.
+ * @return true, if the descriptor was added to the characteristic
+ */
+ public boolean addDescriptor(BluetoothGattDescriptor descriptor) {
+ mDescriptors.add(descriptor);
+ descriptor.setCharacteristic(this);
+ return true;
+ }
+
+ /**
+ * Get a descriptor by UUID and isntance id.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattDescriptor getDescriptor(UUID uuid, int instanceId) {
+ for (BluetoothGattDescriptor descriptor : mDescriptors) {
+ if (descriptor.getUuid().equals(uuid)
+ && descriptor.getInstanceId() == instanceId) {
+ return descriptor;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the service this characteristic belongs to.
+ *
+ * @return The asscociated service
+ */
+ public BluetoothGattService getService() {
+ return mService;
+ }
+
+ /**
+ * Sets the service associated with this device.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ /*package*/ void setService(BluetoothGattService service) {
+ mService = service;
+ }
+
+ /**
+ * Returns the UUID of this characteristic
+ *
+ * @return UUID of this characteristic
+ */
+ public UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns the instance ID for this characteristic.
+ *
+ * <p>If a remote device offers multiple characteristics with the same UUID,
+ * the instance ID is used to distuinguish between characteristics.
+ *
+ * @return Instance ID of this characteristic
+ */
+ public int getInstanceId() {
+ return mInstance;
+ }
+
+ /**
+ * Force the instance ID.
+ *
+ * @hide
+ */
+ public void setInstanceId(int instanceId) {
+ mInstance = instanceId;
+ }
+
+ /**
+ * Returns the properties of this characteristic.
+ *
+ * <p>The properties contain a bit mask of property flags indicating
+ * the features of this characteristic.
+ *
+ * @return Properties of this characteristic
+ */
+ public int getProperties() {
+ return mProperties;
+ }
+
+ /**
+ * Returns the permissions for this characteristic.
+ *
+ * @return Permissions of this characteristic
+ */
+ public int getPermissions() {
+ return mPermissions;
+ }
+
+ /**
+ * Gets the write type for this characteristic.
+ *
+ * @return Write type for this characteristic
+ */
+ public int getWriteType() {
+ return mWriteType;
+ }
+
+ /**
+ * Set the write type for this characteristic
+ *
+ * <p>Setting the write type of a characteristic determines how the
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)} function
+ * write this characteristic.
+ *
+ * @param writeType The write type to for this characteristic. Can be one of: {@link
+ * #WRITE_TYPE_DEFAULT}, {@link #WRITE_TYPE_NO_RESPONSE} or {@link #WRITE_TYPE_SIGNED}.
+ */
+ public void setWriteType(int writeType) {
+ mWriteType = writeType;
+ }
+
+ /**
+ * Set the desired key size.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setKeySize(int keySize) {
+ mKeySize = keySize;
+ }
+
+ /**
+ * Returns a list of descriptors for this characteristic.
+ *
+ * @return Descriptors for this characteristic
+ */
+ public List<BluetoothGattDescriptor> getDescriptors() {
+ return mDescriptors;
+ }
+
+ /**
+ * Returns a descriptor with a given UUID out of the list of
+ * descriptors for this characteristic.
+ *
+ * @return GATT descriptor object or null if no descriptor with the given UUID was found.
+ */
+ public BluetoothGattDescriptor getDescriptor(UUID uuid) {
+ for (BluetoothGattDescriptor descriptor : mDescriptors) {
+ if (descriptor.getUuid().equals(uuid)) {
+ return descriptor;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the stored value for this characteristic.
+ *
+ * <p>This function returns the stored value for this characteristic as
+ * retrieved by calling {@link BluetoothGatt#readCharacteristic}. The cached
+ * value of the characteristic is updated as a result of a read characteristic
+ * operation or if a characteristic update notification has been received.
+ *
+ * @return Cached value of the characteristic
+ *
+ * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} instead
+ */
+ @Deprecated
+ public byte[] getValue() {
+ return mValue;
+ }
+
+ /**
+ * Return the stored value of this characteristic.
+ *
+ * <p>The formatType parameter determines how the characteristic value
+ * is to be interpreted. For example, settting formatType to
+ * {@link #FORMAT_UINT16} specifies that the first two bytes of the
+ * characteristic value at the given offset are interpreted to generate the
+ * return value.
+ *
+ * @param formatType The format type used to interpret the characteristic value.
+ * @param offset Offset at which the integer value can be found.
+ * @return Cached value of the characteristic or null of offset exceeds value size.
+ *
+ * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
+ * the characteristic value
+ */
+ @Deprecated
+ public Integer getIntValue(int formatType, int offset) {
+ if ((offset + getTypeLen(formatType)) > mValue.length) return null;
+
+ switch (formatType) {
+ case FORMAT_UINT8:
+ return unsignedByteToInt(mValue[offset]);
+
+ case FORMAT_UINT16:
+ return unsignedBytesToInt(mValue[offset], mValue[offset + 1]);
+
+ case FORMAT_UINT32:
+ return unsignedBytesToInt(mValue[offset], mValue[offset + 1],
+ mValue[offset + 2], mValue[offset + 3]);
+ case FORMAT_SINT8:
+ return unsignedToSigned(unsignedByteToInt(mValue[offset]), 8);
+
+ case FORMAT_SINT16:
+ return unsignedToSigned(unsignedBytesToInt(mValue[offset],
+ mValue[offset + 1]), 16);
+
+ case FORMAT_SINT32:
+ return unsignedToSigned(unsignedBytesToInt(mValue[offset],
+ mValue[offset + 1], mValue[offset + 2], mValue[offset + 3]), 32);
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the stored value of this characteristic.
+ * <p>See {@link #getValue} for details.
+ *
+ * @param formatType The format type used to interpret the characteristic value.
+ * @param offset Offset at which the float value can be found.
+ * @return Cached value of the characteristic at a given offset or null if the requested offset
+ * exceeds the value size.
+ *
+ * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
+ * the characteristic value
+ */
+ @Deprecated
+ public Float getFloatValue(int formatType, int offset) {
+ if ((offset + getTypeLen(formatType)) > mValue.length) return null;
+
+ switch (formatType) {
+ case FORMAT_SFLOAT:
+ return bytesToFloat(mValue[offset], mValue[offset + 1]);
+
+ case FORMAT_FLOAT:
+ return bytesToFloat(mValue[offset], mValue[offset + 1],
+ mValue[offset + 2], mValue[offset + 3]);
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the stored value of this characteristic.
+ * <p>See {@link #getValue} for details.
+ *
+ * @param offset Offset at which the string value can be found.
+ * @return Cached value of the characteristic
+ *
+ * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
+ * the characteristic value
+ */
+ @Deprecated
+ public String getStringValue(int offset) {
+ if (mValue == null || offset > mValue.length) return null;
+ byte[] strBytes = new byte[mValue.length - offset];
+ for (int i = 0; i != (mValue.length - offset); ++i) strBytes[i] = mValue[offset + i];
+ return new String(strBytes);
+ }
+
+ /**
+ * Updates the locally stored value of this characteristic.
+ *
+ * <p>This function modifies the locally stored cached value of this
+ * characteristic. To send the value to the remote device, call
+ * {@link BluetoothGatt#writeCharacteristic} to send the value to the
+ * remote device.
+ *
+ * @param value New value for this characteristic
+ * @return true if the locally stored value has been set, false if the requested value could not
+ * be stored locally.
+ *
+ * @deprecated Pass the characteristic value directly into
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
+ */
+ @Deprecated
+ public boolean setValue(byte[] value) {
+ mValue = value;
+ return true;
+ }
+
+ /**
+ * Set the locally stored value of this characteristic.
+ * <p>See {@link #setValue(byte[])} for details.
+ *
+ * @param value New value for this characteristic
+ * @param formatType Integer format type used to transform the value parameter
+ * @param offset Offset at which the value should be placed
+ * @return true if the locally stored value has been set
+ *
+ * @deprecated Pass the characteristic value directly into
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
+ */
+ @Deprecated
+ public boolean setValue(int value, int formatType, int offset) {
+ int len = offset + getTypeLen(formatType);
+ if (mValue == null) mValue = new byte[len];
+ if (len > mValue.length) return false;
+
+ switch (formatType) {
+ case FORMAT_SINT8:
+ value = intToSignedBits(value, 8);
+ // Fall-through intended
+ case FORMAT_UINT8:
+ mValue[offset] = (byte) (value & 0xFF);
+ break;
+
+ case FORMAT_SINT16:
+ value = intToSignedBits(value, 16);
+ // Fall-through intended
+ case FORMAT_UINT16:
+ mValue[offset++] = (byte) (value & 0xFF);
+ mValue[offset] = (byte) ((value >> 8) & 0xFF);
+ break;
+
+ case FORMAT_SINT32:
+ value = intToSignedBits(value, 32);
+ // Fall-through intended
+ case FORMAT_UINT32:
+ mValue[offset++] = (byte) (value & 0xFF);
+ mValue[offset++] = (byte) ((value >> 8) & 0xFF);
+ mValue[offset++] = (byte) ((value >> 16) & 0xFF);
+ mValue[offset] = (byte) ((value >> 24) & 0xFF);
+ break;
+
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set the locally stored value of this characteristic.
+ * <p>See {@link #setValue(byte[])} for details.
+ *
+ * @param mantissa Mantissa for this characteristic
+ * @param exponent exponent value for this characteristic
+ * @param formatType Float format type used to transform the value parameter
+ * @param offset Offset at which the value should be placed
+ * @return true if the locally stored value has been set
+ *
+ * @deprecated Pass the characteristic value directly into
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
+ */
+ @Deprecated
+ public boolean setValue(int mantissa, int exponent, int formatType, int offset) {
+ int len = offset + getTypeLen(formatType);
+ if (mValue == null) mValue = new byte[len];
+ if (len > mValue.length) return false;
+
+ switch (formatType) {
+ case FORMAT_SFLOAT:
+ mantissa = intToSignedBits(mantissa, 12);
+ exponent = intToSignedBits(exponent, 4);
+ mValue[offset++] = (byte) (mantissa & 0xFF);
+ mValue[offset] = (byte) ((mantissa >> 8) & 0x0F);
+ mValue[offset] += (byte) ((exponent & 0x0F) << 4);
+ break;
+
+ case FORMAT_FLOAT:
+ mantissa = intToSignedBits(mantissa, 24);
+ exponent = intToSignedBits(exponent, 8);
+ mValue[offset++] = (byte) (mantissa & 0xFF);
+ mValue[offset++] = (byte) ((mantissa >> 8) & 0xFF);
+ mValue[offset++] = (byte) ((mantissa >> 16) & 0xFF);
+ mValue[offset] += (byte) (exponent & 0xFF);
+ break;
+
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Set the locally stored value of this characteristic.
+ * <p>See {@link #setValue(byte[])} for details.
+ *
+ * @param value New value for this characteristic
+ * @return true if the locally stored value has been set
+ *
+ * @deprecated Pass the characteristic value directly into
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
+ */
+ @Deprecated
+ public boolean setValue(String value) {
+ mValue = value.getBytes();
+ return true;
+ }
+
+ /**
+ * Returns the size of a give value type.
+ */
+ private int getTypeLen(int formatType) {
+ return formatType & 0xF;
+ }
+
+ /**
+ * Convert a signed byte to an unsigned int.
+ */
+ private int unsignedByteToInt(byte b) {
+ return b & 0xFF;
+ }
+
+ /**
+ * Convert signed bytes to a 16-bit unsigned int.
+ */
+ private int unsignedBytesToInt(byte b0, byte b1) {
+ return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8));
+ }
+
+ /**
+ * Convert signed bytes to a 32-bit unsigned int.
+ */
+ private int unsignedBytesToInt(byte b0, byte b1, byte b2, byte b3) {
+ return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8))
+ + (unsignedByteToInt(b2) << 16) + (unsignedByteToInt(b3) << 24);
+ }
+
+ /**
+ * Convert signed bytes to a 16-bit short float value.
+ */
+ private float bytesToFloat(byte b0, byte b1) {
+ int mantissa = unsignedToSigned(unsignedByteToInt(b0)
+ + ((unsignedByteToInt(b1) & 0x0F) << 8), 12);
+ int exponent = unsignedToSigned(unsignedByteToInt(b1) >> 4, 4);
+ return (float) (mantissa * Math.pow(10, exponent));
+ }
+
+ /**
+ * Convert signed bytes to a 32-bit short float value.
+ */
+ private float bytesToFloat(byte b0, byte b1, byte b2, byte b3) {
+ int mantissa = unsignedToSigned(unsignedByteToInt(b0)
+ + (unsignedByteToInt(b1) << 8)
+ + (unsignedByteToInt(b2) << 16), 24);
+ return (float) (mantissa * Math.pow(10, b3));
+ }
+
+ /**
+ * Convert an unsigned integer value to a two's-complement encoded
+ * signed value.
+ */
+ private int unsignedToSigned(int unsigned, int size) {
+ if ((unsigned & (1 << size - 1)) != 0) {
+ unsigned = -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
+ }
+ return unsigned;
+ }
+
+ /**
+ * Convert an integer into the signed bits of a given length.
+ */
+ private int intToSignedBits(int i, int size) {
+ if (i < 0) {
+ i = (1 << size - 1) + (i & ((1 << size - 1) - 1));
+ }
+ return i;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothGattDescriptor.java b/android-34/android/bluetooth/BluetoothGattDescriptor.java
new file mode 100644
index 0000000..0bb86f5
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothGattDescriptor.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.util.UUID;
+
+/**
+ * Represents a Bluetooth GATT Descriptor
+ *
+ * <p> GATT Descriptors contain additional information and attributes of a GATT
+ * characteristic, {@link BluetoothGattCharacteristic}. They can be used to describe
+ * the characteristic's features or to control certain behaviours of the characteristic.
+ */
+public class BluetoothGattDescriptor implements Parcelable {
+
+ /**
+ * Value used to enable notification for a client configuration descriptor
+ */
+ public static final byte[] ENABLE_NOTIFICATION_VALUE = {0x01, 0x00};
+
+ /**
+ * Value used to enable indication for a client configuration descriptor
+ */
+ public static final byte[] ENABLE_INDICATION_VALUE = {0x02, 0x00};
+
+ /**
+ * Value used to disable notifications or indicatinos
+ */
+ public static final byte[] DISABLE_NOTIFICATION_VALUE = {0x00, 0x00};
+
+ /**
+ * Descriptor read permission
+ */
+ public static final int PERMISSION_READ = 0x01;
+
+ /**
+ * Descriptor permission: Allow encrypted read operations
+ */
+ public static final int PERMISSION_READ_ENCRYPTED = 0x02;
+
+ /**
+ * Descriptor permission: Allow reading with person-in-the-middle protection
+ */
+ public static final int PERMISSION_READ_ENCRYPTED_MITM = 0x04;
+
+ /**
+ * Descriptor write permission
+ */
+ public static final int PERMISSION_WRITE = 0x10;
+
+ /**
+ * Descriptor permission: Allow encrypted writes
+ */
+ public static final int PERMISSION_WRITE_ENCRYPTED = 0x20;
+
+ /**
+ * Descriptor permission: Allow encrypted writes with person-in-the-middle
+ * protection
+ */
+ public static final int PERMISSION_WRITE_ENCRYPTED_MITM = 0x40;
+
+ /**
+ * Descriptor permission: Allow signed write operations
+ */
+ public static final int PERMISSION_WRITE_SIGNED = 0x80;
+
+ /**
+ * Descriptor permission: Allow signed write operations with
+ * person-in-the-middle protection
+ */
+ public static final int PERMISSION_WRITE_SIGNED_MITM = 0x100;
+
+ /**
+ * The UUID of this descriptor.
+ *
+ * @hide
+ */
+ protected UUID mUuid;
+
+ /**
+ * Instance ID for this descriptor.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected int mInstance;
+
+ /**
+ * Permissions for this descriptor
+ *
+ * @hide
+ */
+ protected int mPermissions;
+
+ /**
+ * Back-reference to the characteristic this descriptor belongs to.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected BluetoothGattCharacteristic mCharacteristic;
+
+ /**
+ * The value for this descriptor.
+ *
+ * @hide
+ */
+ protected byte[] mValue;
+
+ /**
+ * Create a new BluetoothGattDescriptor.
+ *
+ * @param uuid The UUID for this descriptor
+ * @param permissions Permissions for this descriptor
+ */
+ public BluetoothGattDescriptor(UUID uuid, int permissions) {
+ initDescriptor(null, uuid, 0, permissions);
+ }
+
+ /**
+ * Create a new BluetoothGattDescriptor.
+ *
+ * @param characteristic The characteristic this descriptor belongs to
+ * @param uuid The UUID for this descriptor
+ * @param permissions Permissions for this descriptor
+ */
+ /*package*/ BluetoothGattDescriptor(BluetoothGattCharacteristic characteristic, UUID uuid,
+ int instance, int permissions) {
+ initDescriptor(characteristic, uuid, instance, permissions);
+ }
+
+ /**
+ * @hide
+ */
+ public BluetoothGattDescriptor(UUID uuid, int instance, int permissions) {
+ initDescriptor(null, uuid, instance, permissions);
+ }
+
+ private void initDescriptor(BluetoothGattCharacteristic characteristic, UUID uuid,
+ int instance, int permissions) {
+ mCharacteristic = characteristic;
+ mUuid = uuid;
+ mInstance = instance;
+ mPermissions = permissions;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeInt(mInstance);
+ out.writeInt(mPermissions);
+ }
+
+ public static final @NonNull Creator<BluetoothGattDescriptor> CREATOR = new Creator<>() {
+ public BluetoothGattDescriptor createFromParcel(Parcel in) {
+ return new BluetoothGattDescriptor(in);
+ }
+
+ public BluetoothGattDescriptor[] newArray(int size) {
+ return new BluetoothGattDescriptor[size];
+ }
+ };
+
+ private BluetoothGattDescriptor(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mInstance = in.readInt();
+ mPermissions = in.readInt();
+ }
+
+ /**
+ * Returns the characteristic this descriptor belongs to.
+ *
+ * @return The characteristic.
+ */
+ public BluetoothGattCharacteristic getCharacteristic() {
+ return mCharacteristic;
+ }
+
+ /**
+ * Set the back-reference to the associated characteristic
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ /*package*/ void setCharacteristic(BluetoothGattCharacteristic characteristic) {
+ mCharacteristic = characteristic;
+ }
+
+ /**
+ * Returns the UUID of this descriptor.
+ *
+ * @return UUID of this descriptor
+ */
+ public UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns the instance ID for this descriptor.
+ *
+ * <p>If a remote device offers multiple descriptors with the same UUID,
+ * the instance ID is used to distuinguish between descriptors.
+ *
+ * @return Instance ID of this descriptor
+ * @hide
+ */
+ public int getInstanceId() {
+ return mInstance;
+ }
+
+ /**
+ * Force the instance ID.
+ *
+ * @hide
+ */
+ public void setInstanceId(int instanceId) {
+ mInstance = instanceId;
+ }
+
+ /**
+ * Returns the permissions for this descriptor.
+ *
+ * @return Permissions of this descriptor
+ */
+ public int getPermissions() {
+ return mPermissions;
+ }
+
+ /**
+ * Returns the stored value for this descriptor
+ *
+ * <p>This function returns the stored value for this descriptor as
+ * retrieved by calling {@link BluetoothGatt#readDescriptor}. The cached
+ * value of the descriptor is updated as a result of a descriptor read
+ * operation.
+ *
+ * @return Cached value of the descriptor
+ *
+ * @deprecated Use {@link BluetoothGatt#readDescriptor(BluetoothGattDescriptor)} instead
+ */
+ @Deprecated
+ public byte[] getValue() {
+ return mValue;
+ }
+
+ /**
+ * Updates the locally stored value of this descriptor.
+ *
+ * <p>This function modifies the locally stored cached value of this
+ * descriptor. To send the value to the remote device, call
+ * {@link BluetoothGatt#writeDescriptor} to send the value to the
+ * remote device.
+ *
+ * @param value New value for this descriptor
+ * @return true if the locally stored value has been set, false if the requested value could not
+ * be stored locally.
+ *
+ * @deprecated Pass the descriptor value directly into
+ * {@link BluetoothGatt#writeDescriptor(BluetoothGattDescriptor, byte[])}
+ */
+ @Deprecated
+ public boolean setValue(byte[] value) {
+ mValue = value;
+ return true;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothGattIncludedService.java b/android-34/android/bluetooth/BluetoothGattIncludedService.java
new file mode 100644
index 0000000..a33f8cc
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothGattIncludedService.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.util.UUID;
+
+/**
+ * Represents a Bluetooth GATT Included Service
+ *
+ * @hide
+ */
+public class BluetoothGattIncludedService implements Parcelable {
+
+ /**
+ * The UUID of this service.
+ */
+ protected UUID mUuid;
+
+ /**
+ * Instance ID for this service.
+ */
+ protected int mInstanceId;
+
+ /**
+ * Service type (Primary/Secondary).
+ */
+ protected int mServiceType;
+
+ /**
+ * Create a new BluetoothGattIncludedService
+ */
+ public BluetoothGattIncludedService(UUID uuid, int instanceId, int serviceType) {
+ mUuid = uuid;
+ mInstanceId = instanceId;
+ mServiceType = serviceType;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeInt(mInstanceId);
+ out.writeInt(mServiceType);
+ }
+
+ public static final @NonNull Creator<BluetoothGattIncludedService> CREATOR = new Creator<>() {
+ public BluetoothGattIncludedService createFromParcel(Parcel in) {
+ return new BluetoothGattIncludedService(in);
+ }
+
+ public BluetoothGattIncludedService[] newArray(int size) {
+ return new BluetoothGattIncludedService[size];
+ }
+ };
+
+ private BluetoothGattIncludedService(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mInstanceId = in.readInt();
+ mServiceType = in.readInt();
+ }
+
+ /**
+ * Returns the UUID of this service
+ *
+ * @return UUID of this service
+ */
+ public UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns the instance ID for this service
+ *
+ * <p>If a remote device offers multiple services with the same UUID
+ * (ex. multiple battery services for different batteries), the instance
+ * ID is used to distuinguish services.
+ *
+ * @return Instance ID of this service
+ */
+ public int getInstanceId() {
+ return mInstanceId;
+ }
+
+ /**
+ * Get the type of this service (primary/secondary)
+ */
+ public int getType() {
+ return mServiceType;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothGattServer.java b/android-34/android/bluetooth/BluetoothGattServer.java
new file mode 100644
index 0000000..05eaf1a
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothGattServer.java
@@ -0,0 +1,1019 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.content.AttributionSource;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Public API for the Bluetooth GATT Profile server role.
+ *
+ * <p>This class provides Bluetooth GATT server role functionality,
+ * allowing applications to create Bluetooth Smart services and
+ * characteristics.
+ *
+ * <p>BluetoothGattServer is a proxy object for controlling the Bluetooth Service
+ * via IPC. Use {@link BluetoothManager#openGattServer} to get an instance
+ * of this class.
+ */
+public final class BluetoothGattServer implements BluetoothProfile {
+ private static final String TAG = "BluetoothGattServer";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ private final IBluetoothGatt mService;
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+
+ private BluetoothGattServerCallback mCallback;
+
+ private Object mServerIfLock = new Object();
+ private int mServerIf;
+ private int mTransport;
+ private BluetoothGattService mPendingService;
+ private List<BluetoothGattService> mServices;
+
+ private static final int CALLBACK_REG_TIMEOUT = 10000;
+
+ /**
+ * Bluetooth GATT interface callbacks
+ */
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothGattServerCallback mBluetoothGattServerCallback =
+ new IBluetoothGattServerCallback.Stub() {
+ /**
+ * Application interface registered - app is ready to go
+ * @hide
+ */
+ @Override
+ public void onServerRegistered(int status, int serverIf) {
+ if (DBG) {
+ Log.d(TAG, "onServerRegistered() - status=" + status
+ + " serverIf=" + serverIf);
+ }
+ synchronized (mServerIfLock) {
+ if (mCallback != null) {
+ mServerIf = serverIf;
+ mServerIfLock.notify();
+ } else {
+ // registration timeout
+ Log.e(TAG, "onServerRegistered: mCallback is null");
+ }
+ }
+ }
+
+ /**
+ * Server connection state changed
+ * @hide
+ */
+ @Override
+ public void onServerConnectionState(int status, int serverIf,
+ boolean connected, String address) {
+ if (DBG) {
+ Log.d(TAG, "onServerConnectionState() - status=" + status
+ + " serverIf=" + serverIf + " device=" + address);
+ }
+ try {
+ mCallback.onConnectionStateChange(mAdapter.getRemoteDevice(address), status,
+ connected ? BluetoothProfile.STATE_CONNECTED :
+ BluetoothProfile.STATE_DISCONNECTED);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Service has been added
+ * @hide
+ */
+ @Override
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ if (DBG) {
+ Log.d(TAG, "onServiceAdded() - handle=" + service.getInstanceId()
+ + " uuid=" + service.getUuid() + " status=" + status);
+ }
+
+ if (mPendingService == null) {
+ return;
+ }
+
+ BluetoothGattService tmp = mPendingService;
+ mPendingService = null;
+
+ // Rewrite newly assigned handles to existing service.
+ tmp.setInstanceId(service.getInstanceId());
+ List<BluetoothGattCharacteristic> temp_chars = tmp.getCharacteristics();
+ List<BluetoothGattCharacteristic> svc_chars = service.getCharacteristics();
+ for (int i = 0; i < svc_chars.size(); i++) {
+ BluetoothGattCharacteristic temp_char = temp_chars.get(i);
+ BluetoothGattCharacteristic svc_char = svc_chars.get(i);
+
+ temp_char.setInstanceId(svc_char.getInstanceId());
+
+ List<BluetoothGattDescriptor> temp_descs = temp_char.getDescriptors();
+ List<BluetoothGattDescriptor> svc_descs = svc_char.getDescriptors();
+ for (int j = 0; j < svc_descs.size(); j++) {
+ temp_descs.get(j).setInstanceId(svc_descs.get(j).getInstanceId());
+ }
+ }
+
+ mServices.add(tmp);
+
+ try {
+ mCallback.onServiceAdded((int) status, tmp);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Remote client characteristic read request.
+ * @hide
+ */
+ @Override
+ public void onCharacteristicReadRequest(String address, int transId,
+ int offset, boolean isLong, int handle) {
+ if (VDBG) Log.d(TAG, "onCharacteristicReadRequest() - handle=" + handle);
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ BluetoothGattCharacteristic characteristic = getCharacteristicByHandle(handle);
+ if (characteristic == null) {
+ Log.w(TAG, "onCharacteristicReadRequest() no char for handle " + handle);
+ return;
+ }
+
+ try {
+ mCallback.onCharacteristicReadRequest(device, transId, offset,
+ characteristic);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Remote client descriptor read request.
+ * @hide
+ */
+ @Override
+ public void onDescriptorReadRequest(String address, int transId,
+ int offset, boolean isLong, int handle) {
+ if (VDBG) Log.d(TAG, "onCharacteristicReadRequest() - handle=" + handle);
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ BluetoothGattDescriptor descriptor = getDescriptorByHandle(handle);
+ if (descriptor == null) {
+ Log.w(TAG, "onDescriptorReadRequest() no desc for handle " + handle);
+ return;
+ }
+
+ try {
+ mCallback.onDescriptorReadRequest(device, transId, offset, descriptor);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Remote client characteristic write request.
+ * @hide
+ */
+ @Override
+ public void onCharacteristicWriteRequest(String address, int transId,
+ int offset, int length, boolean isPrep, boolean needRsp,
+ int handle, byte[] value) {
+ if (VDBG) Log.d(TAG, "onCharacteristicWriteRequest() - handle=" + handle);
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ BluetoothGattCharacteristic characteristic = getCharacteristicByHandle(handle);
+ if (characteristic == null) {
+ Log.w(TAG, "onCharacteristicWriteRequest() no char for handle " + handle);
+ return;
+ }
+
+ try {
+ mCallback.onCharacteristicWriteRequest(device, transId, characteristic,
+ isPrep, needRsp, offset, value);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+
+ }
+
+ /**
+ * Remote client descriptor write request.
+ * @hide
+ */
+ @Override
+ public void onDescriptorWriteRequest(String address, int transId, int offset,
+ int length, boolean isPrep, boolean needRsp, int handle, byte[] value) {
+ if (VDBG) Log.d(TAG, "onDescriptorWriteRequest() - handle=" + handle);
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ BluetoothGattDescriptor descriptor = getDescriptorByHandle(handle);
+ if (descriptor == null) {
+ Log.w(TAG, "onDescriptorWriteRequest() no desc for handle " + handle);
+ return;
+ }
+
+ try {
+ mCallback.onDescriptorWriteRequest(device, transId, descriptor,
+ isPrep, needRsp, offset, value);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Execute pending writes.
+ * @hide
+ */
+ @Override
+ public void onExecuteWrite(String address, int transId,
+ boolean execWrite) {
+ if (DBG) {
+ Log.d(TAG, "onExecuteWrite() - "
+ + "device=" + address + ", transId=" + transId
+ + "execWrite=" + execWrite);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onExecuteWrite(device, transId, execWrite);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * A notification/indication has been sent.
+ * @hide
+ */
+ @Override
+ public void onNotificationSent(String address, int status) {
+ if (VDBG) {
+ Log.d(TAG, "onNotificationSent() - "
+ + "device=" + address + ", status=" + status);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onNotificationSent(device, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * The MTU for a connection has changed
+ * @hide
+ */
+ @Override
+ public void onMtuChanged(String address, int mtu) {
+ if (DBG) {
+ Log.d(TAG, "onMtuChanged() - "
+ + "device=" + address + ", mtu=" + mtu);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onMtuChanged(device, mtu);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * The PHY for a connection was updated
+ * @hide
+ */
+ @Override
+ public void onPhyUpdate(String address, int txPhy, int rxPhy, int status) {
+ if (DBG) {
+ Log.d(TAG,
+ "onPhyUpdate() - " + "device=" + address + ", txPHy=" + txPhy
+ + ", rxPHy=" + rxPhy);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onPhyUpdate(device, txPhy, rxPhy, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * The PHY for a connection was read
+ * @hide
+ */
+ @Override
+ public void onPhyRead(String address, int txPhy, int rxPhy, int status) {
+ if (DBG) {
+ Log.d(TAG,
+ "onPhyUpdate() - " + "device=" + address + ", txPHy=" + txPhy
+ + ", rxPHy=" + rxPhy);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onPhyRead(device, txPhy, rxPhy, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * Callback invoked when the given connection is updated
+ * @hide
+ */
+ @Override
+ public void onConnectionUpdated(String address, int interval, int latency,
+ int timeout, int status) {
+ if (DBG) {
+ Log.d(TAG, "onConnectionUpdated() - Device=" + address
+ + " interval=" + interval + " latency=" + latency
+ + " timeout=" + timeout + " status=" + status);
+ }
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onConnectionUpdated(device, interval, latency,
+ timeout, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * Callback invoked when the given connection's subrate parameters are changed
+ * @hide
+ */
+ @Override
+ public void onSubrateChange(String address, int subrateFactor, int latency,
+ int contNum, int timeout, int status) {
+ if (DBG) {
+ Log.d(TAG,
+ "onSubrateChange() - "
+ + "Device=" + BluetoothUtils.toAnonymizedAddress(address)
+ + ", subrateFactor=" + subrateFactor
+ + ", latency=" + latency + ", contNum=" + contNum
+ + ", timeout=" + timeout + ", status=" + status);
+ }
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) {
+ return;
+ }
+
+ try {
+ mCallback.onSubrateChange(
+ device, subrateFactor, latency, contNum, timeout, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+ };
+
+ /**
+ * Create a BluetoothGattServer proxy object.
+ */
+ /* package */ BluetoothGattServer(IBluetoothGatt iGatt, int transport,
+ BluetoothAdapter adapter) {
+ mService = iGatt;
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mCallback = null;
+ mServerIf = 0;
+ mTransport = transport;
+ mServices = new ArrayList<BluetoothGattService>();
+ }
+
+ /**
+ * Get the identifier of the BluetoothGattServer, or 0 if it is closed
+ *
+ * @hide
+ */
+ public int getServerIf() {
+ return mServerIf;
+ }
+
+ /**
+ * Returns a characteristic with given handle.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattCharacteristic getCharacteristicByHandle(int handle) {
+ for (BluetoothGattService svc : mServices) {
+ for (BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
+ if (charac.getInstanceId() == handle) {
+ return charac;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a descriptor with given handle.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattDescriptor getDescriptorByHandle(int handle) {
+ for (BluetoothGattService svc : mServices) {
+ for (BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
+ for (BluetoothGattDescriptor desc : charac.getDescriptors()) {
+ if (desc.getInstanceId() == handle) {
+ return desc;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Close this GATT server instance.
+ *
+ * <p>Application should call this method as early as possible after it is done with this GATT
+ * server.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @Override
+ public void close() {
+ if (DBG) Log.d(TAG, "close()");
+ unregisterCallback();
+ }
+
+ /**
+ * Register an application callback to start using GattServer.
+ *
+ * <p>This is an asynchronous call. The callback is used to notify
+ * success or failure if the function returns true.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @return true, the callback will be called to notify success or failure, false on immediate
+ * error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ /*package*/ boolean registerCallback(BluetoothGattServerCallback callback) {
+ return registerCallback(callback, false);
+ }
+
+ /**
+ * Register an application callback to start using GattServer.
+ *
+ * <p>This is an asynchronous call. The callback is used to notify
+ * success or failure if the function returns true.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param eattSupport indicates if server can use eatt
+ * @return true, the callback will be called to notify success or failure, false on immediate
+ * error
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ /*package*/ boolean registerCallback(BluetoothGattServerCallback callback,
+ boolean eattSupport) {
+ if (DBG) Log.d(TAG, "registerCallback()");
+ if (mService == null) {
+ Log.e(TAG, "GATT service not available");
+ return false;
+ }
+ UUID uuid = UUID.randomUUID();
+ if (DBG) Log.d(TAG, "registerCallback() - UUID=" + uuid);
+
+ synchronized (mServerIfLock) {
+ if (mCallback != null) {
+ Log.e(TAG, "App can register callback only once");
+ return false;
+ }
+
+ mCallback = callback;
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.registerServer(new ParcelUuid(uuid), mBluetoothGattServerCallback,
+ eattSupport, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ mCallback = null;
+ return false;
+ }
+
+ try {
+ mServerIfLock.wait(CALLBACK_REG_TIMEOUT);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "" + e);
+ mCallback = null;
+ }
+
+ if (mServerIf == 0) {
+ mCallback = null;
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Unregister the current application and callbacks.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private void unregisterCallback() {
+ if (DBG) Log.d(TAG, "unregisterCallback() - mServerIf=" + mServerIf);
+ if (mService == null || mServerIf == 0) return;
+
+ try {
+ mCallback = null;
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.unregisterServer(mServerIf, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ mServerIf = 0;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Returns a service by UUID, instance and type.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattService getService(UUID uuid, int instanceId, int type) {
+ for (BluetoothGattService svc : mServices) {
+ if (svc.getType() == type
+ && svc.getInstanceId() == instanceId
+ && svc.getUuid().equals(uuid)) {
+ return svc;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Initiate a connection to a Bluetooth GATT capable device.
+ *
+ * <p>The connection may not be established right away, but will be
+ * completed when the remote device is available. A
+ * {@link BluetoothGattServerCallback#onConnectionStateChange} callback will be
+ * invoked when the connection state changes as a result of this function.
+ *
+ * <p>The autoConnect parameter determines whether to actively connect to
+ * the remote device, or rather passively scan and finalize the connection
+ * when the remote device is in range/available. Generally, the first ever
+ * connection to a device should be direct (autoConnect set to false) and
+ * subsequent connections to known devices should be invoked with the
+ * autoConnect parameter set to true.
+ *
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @return true, if the connection attempt was initiated successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connect(BluetoothDevice device, boolean autoConnect) {
+ if (DBG) {
+ Log.d(TAG,
+ "connect() - device: " + device + ", auto: " + autoConnect);
+ }
+ if (mService == null || mServerIf == 0) return false;
+
+ try {
+ // autoConnect is inverse of "isDirect"
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.serverConnect(mServerIf, device.getAddress(), !autoConnect, mTransport,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Disconnects an established connection, or cancels a connection attempt
+ * currently in progress.
+ *
+ * @param device Remote device
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void cancelConnection(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "cancelConnection() - device: " + device);
+ if (mService == null || mServerIf == 0) return;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.serverDisconnect(mServerIf, device.getAddress(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Set the preferred connection PHY for this app. Please note that this is just a
+ * recommendation, whether the PHY change will happen depends on other applications peferences,
+ * local and remote controller capabilities. Controller can override these settings. <p> {@link
+ * BluetoothGattServerCallback#onPhyUpdate} will be triggered as a result of this call, even if
+ * no PHY change happens. It is also triggered when remote device updates the PHY.
+ *
+ * @param device The remote device to send this response to
+ * @param txPhy preferred transmitter PHY. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}.
+ * @param rxPhy preferred receiver PHY. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}.
+ * @param phyOptions preferred coding to use when transmitting on the LE Coded PHY. Can be one
+ * of {@link BluetoothDevice#PHY_OPTION_NO_PREFERRED}, {@link BluetoothDevice#PHY_OPTION_S2} or
+ * {@link BluetoothDevice#PHY_OPTION_S8}
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setPreferredPhy(BluetoothDevice device, int txPhy, int rxPhy, int phyOptions) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.serverSetPreferredPhy(mServerIf, device.getAddress(), txPhy, rxPhy,
+ phyOptions, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Read the current transmitter PHY and receiver PHY of the connection. The values are returned
+ * in {@link BluetoothGattServerCallback#onPhyRead}
+ *
+ * @param device The remote device to send this response to
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void readPhy(BluetoothDevice device) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.serverReadPhy(mServerIf, device.getAddress(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Send a response to a read or write request to a remote device.
+ *
+ * <p>This function must be invoked in when a remote read/write request
+ * is received by one of these callback methods:
+ *
+ * <ul>
+ * <li>{@link BluetoothGattServerCallback#onCharacteristicReadRequest}
+ * <li>{@link BluetoothGattServerCallback#onCharacteristicWriteRequest}
+ * <li>{@link BluetoothGattServerCallback#onDescriptorReadRequest}
+ * <li>{@link BluetoothGattServerCallback#onDescriptorWriteRequest}
+ * </ul>
+ *
+ * @param device The remote device to send this response to
+ * @param requestId The ID of the request that was received with the callback
+ * @param status The status of the request to be sent to the remote devices
+ * @param offset Value offset for partial read/write response
+ * @param value The value of the attribute that was read/written (optional)
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendResponse(BluetoothDevice device, int requestId,
+ int status, int offset, byte[] value) {
+ if (VDBG) Log.d(TAG, "sendResponse() - device: " + device);
+ if (mService == null || mServerIf == 0) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.sendResponse(mServerIf, device.getAddress(), requestId,
+ status, offset, value, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send a notification or indication that a local characteristic has been
+ * updated.
+ *
+ * <p>A notification or indication is sent to the remote device to signal
+ * that the characteristic has been updated. This function should be invoked
+ * for every client that requests notifications/indications by writing
+ * to the "Client Configuration" descriptor for the given characteristic.
+ *
+ * @param device The remote device to receive the notification/indication
+ * @param characteristic The local characteristic that has been updated
+ * @param confirm true to request confirmation from the client (indication), false to send a
+ * notification
+ * @return true, if the notification has been triggered successfully
+ * @throws IllegalArgumentException
+ *
+ * @deprecated Use {@link BluetoothGattServer#notifyCharacteristicChanged(BluetoothDevice,
+ * BluetoothGattCharacteristic, boolean, byte[])} as this is not memory safe.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean notifyCharacteristicChanged(BluetoothDevice device,
+ BluetoothGattCharacteristic characteristic, boolean confirm) {
+ return notifyCharacteristicChanged(device, characteristic, confirm,
+ characteristic.getValue()) == BluetoothStatusCodes.SUCCESS;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ BluetoothStatusCodes.ERROR_UNKNOWN
+ })
+ public @interface NotifyCharacteristicReturnValues{}
+
+ /**
+ * Send a notification or indication that a local characteristic has been
+ * updated.
+ *
+ * <p>A notification or indication is sent to the remote device to signal
+ * that the characteristic has been updated. This function should be invoked
+ * for every client that requests notifications/indications by writing
+ * to the "Client Configuration" descriptor for the given characteristic.
+ *
+ * @param device the remote device to receive the notification/indication
+ * @param characteristic the local characteristic that has been updated
+ * @param confirm {@code true} to request confirmation from the client (indication) or
+ * {@code false} to send a notification
+ * @param value the characteristic value
+ * @return whether the notification has been triggered successfully
+ * @throws IllegalArgumentException if the characteristic value or service is null
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @NotifyCharacteristicReturnValues
+ public int notifyCharacteristicChanged(@NonNull BluetoothDevice device,
+ @NonNull BluetoothGattCharacteristic characteristic, boolean confirm,
+ @NonNull byte[] value) {
+ if (VDBG) Log.d(TAG, "notifyCharacteristicChanged() - device: " + device);
+ if (mService == null || mServerIf == 0) {
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ }
+
+ if (characteristic == null) {
+ throw new IllegalArgumentException("characteristic must not be null");
+ }
+ if (device == null) {
+ throw new IllegalArgumentException("device must not be null");
+ }
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) {
+ throw new IllegalArgumentException("Characteristic must have a non-null service");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("Characteristic value must not be null");
+ }
+
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ mService.sendNotification(mServerIf, device.getAddress(),
+ characteristic.getInstanceId(), confirm,
+ value, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout())
+ .getValue(BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Add a service to the list of services to be hosted.
+ *
+ * <p>Once a service has been addded to the list, the service and its
+ * included characteristics will be provided by the local device.
+ *
+ * <p>If the local device has already exposed services when this function
+ * is called, a service update notification will be sent to all clients.
+ *
+ * <p>The {@link BluetoothGattServerCallback#onServiceAdded} callback will indicate
+ * whether this service has been added successfully. Do not add another service
+ * before this callback.
+ *
+ * @param service Service to be added to the list of services provided by this device.
+ * @return true, if the request to add service has been initiated
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean addService(BluetoothGattService service) {
+ if (DBG) Log.d(TAG, "addService() - service: " + service.getUuid());
+ if (mService == null || mServerIf == 0) return false;
+
+ mPendingService = service;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.addService(mServerIf, service, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Removes a service from the list of services to be provided.
+ *
+ * @param service Service to be removed.
+ * @return true, if the service has been removed
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean removeService(BluetoothGattService service) {
+ if (DBG) Log.d(TAG, "removeService() - service: " + service.getUuid());
+ if (mService == null || mServerIf == 0) return false;
+
+ BluetoothGattService intService = getService(service.getUuid(),
+ service.getInstanceId(), service.getType());
+ if (intService == null) return false;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.removeService(mServerIf, service.getInstanceId(), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ mServices.remove(intService);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove all services from the list of provided services.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void clearServices() {
+ if (DBG) Log.d(TAG, "clearServices()");
+ if (mService == null || mServerIf == 0) return;
+
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ mService.clearServices(mServerIf, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ mServices.clear();
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Returns a list of GATT services offered by this device.
+ *
+ * <p>An application must call {@link #addService} to add a serice to the
+ * list of services offered by this device.
+ *
+ * @return List of services. Returns an empty list if no services have been added yet.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public List<BluetoothGattService> getServices() {
+ return mServices;
+ }
+
+ /**
+ * Returns a {@link BluetoothGattService} from the list of services offered
+ * by this device.
+ *
+ * <p>If multiple instances of the same service (as identified by UUID)
+ * exist, the first instance of the service is returned.
+ *
+ * @param uuid UUID of the requested service
+ * @return BluetoothGattService if supported, or null if the requested service is not offered by
+ * this device.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public BluetoothGattService getService(UUID uuid) {
+ for (BluetoothGattService service : mServices) {
+ if (service.getUuid().equals(uuid)) {
+ return service;
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Not supported - please use {@link BluetoothManager#getConnectedDevices(int)}
+ * with {@link BluetoothProfile#GATT} as argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ public int getConnectionState(BluetoothDevice device) {
+ throw new UnsupportedOperationException("Use BluetoothManager#getConnectionState instead.");
+ }
+
+ /**
+ * Not supported - please use {@link BluetoothManager#getConnectedDevices(int)}
+ * with {@link BluetoothProfile#GATT} as argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ public List<BluetoothDevice> getConnectedDevices() {
+ throw new UnsupportedOperationException(
+ "Use BluetoothManager#getConnectedDevices instead.");
+ }
+
+ /**
+ * Not supported - please use
+ * {@link BluetoothManager#getDevicesMatchingConnectionStates(int, int[])}
+ * with {@link BluetoothProfile#GATT} as first argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ throw new UnsupportedOperationException(
+ "Use BluetoothManager#getDevicesMatchingConnectionStates instead.");
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothGattServerCallback.java b/android-34/android/bluetooth/BluetoothGattServerCallback.java
new file mode 100644
index 0000000..2b600bf
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothGattServerCallback.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+/**
+ * This abstract class is used to implement {@link BluetoothGattServer} callbacks.
+ */
+public abstract class BluetoothGattServerCallback {
+
+ /**
+ * Callback indicating when a remote device has been connected or disconnected.
+ *
+ * @param device Remote device that has been connected or disconnected.
+ * @param status Status of the connect or disconnect operation.
+ * @param newState Returns the new connection state. Can be one of {@link
+ * BluetoothProfile#STATE_DISCONNECTED} or {@link BluetoothProfile#STATE_CONNECTED}
+ */
+ public void onConnectionStateChange(BluetoothDevice device, int status,
+ int newState) {
+ }
+
+ /**
+ * Indicates whether a local service has been added successfully.
+ *
+ * @param status Returns {@link BluetoothGatt#GATT_SUCCESS} if the service was added
+ * successfully.
+ * @param service The service that has been added
+ */
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ }
+
+ /**
+ * A remote client has requested to read a local characteristic.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the read operation
+ * @param requestId The Id of the request
+ * @param offset Offset into the value of the characteristic
+ * @param characteristic Characteristic to be read
+ */
+ public void onCharacteristicReadRequest(BluetoothDevice device, int requestId,
+ int offset, BluetoothGattCharacteristic characteristic) {
+ }
+
+ /**
+ * A remote client has requested to write to a local characteristic.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the write operation
+ * @param requestId The Id of the request
+ * @param characteristic Characteristic to be written to.
+ * @param preparedWrite true, if this write operation should be queued for later execution.
+ * @param responseNeeded true, if the remote device requires a response
+ * @param offset The offset given for the value
+ * @param value The value the client wants to assign to the characteristic
+ */
+ public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite, boolean responseNeeded,
+ int offset, byte[] value) {
+ }
+
+ /**
+ * A remote client has requested to read a local descriptor.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the read operation
+ * @param requestId The Id of the request
+ * @param offset Offset into the value of the characteristic
+ * @param descriptor Descriptor to be read
+ */
+ public void onDescriptorReadRequest(BluetoothDevice device, int requestId,
+ int offset, BluetoothGattDescriptor descriptor) {
+ }
+
+ /**
+ * A remote client has requested to write to a local descriptor.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the write operation
+ * @param requestId The Id of the request
+ * @param descriptor Descriptor to be written to.
+ * @param preparedWrite true, if this write operation should be queued for later execution.
+ * @param responseNeeded true, if the remote device requires a response
+ * @param offset The offset given for the value
+ * @param value The value the client wants to assign to the descriptor
+ */
+ public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattDescriptor descriptor,
+ boolean preparedWrite, boolean responseNeeded,
+ int offset, byte[] value) {
+ }
+
+ /**
+ * Execute all pending write operations for this device.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the write operations
+ * @param requestId The Id of the request
+ * @param execute Whether the pending writes should be executed (true) or cancelled (false)
+ */
+ public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
+ }
+
+ /**
+ * Callback invoked when a notification or indication has been sent to
+ * a remote device.
+ *
+ * <p>When multiple notifications are to be sent, an application must
+ * wait for this callback to be received before sending additional
+ * notifications.
+ *
+ * @param device The remote device the notification has been sent to
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the operation was successful
+ */
+ public void onNotificationSent(BluetoothDevice device, int status) {
+ }
+
+ /**
+ * Callback indicating the MTU for a given device connection has changed.
+ *
+ * <p>This callback will be invoked if a remote client has requested to change
+ * the MTU for a given connection.
+ *
+ * @param device The remote device that requested the MTU change
+ * @param mtu The new MTU size
+ */
+ public void onMtuChanged(BluetoothDevice device, int mtu) {
+ }
+
+ /**
+ * Callback triggered as result of {@link BluetoothGattServer#setPreferredPhy}, or as a result
+ * of remote device changing the PHY.
+ *
+ * @param device The remote device
+ * @param txPhy the transmitter PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}
+ * @param rxPhy the receiver PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}
+ * @param status Status of the PHY update operation. {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) {
+ }
+
+ /**
+ * Callback triggered as result of {@link BluetoothGattServer#readPhy}
+ *
+ * @param device The remote device that requested the PHY read
+ * @param txPhy the transmitter PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}
+ * @param rxPhy the receiver PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}
+ * @param status Status of the PHY read operation. {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) {
+ }
+
+ /**
+ * Callback indicating the connection parameters were updated.
+ *
+ * @param device The remote device involved
+ * @param interval Connection interval used on this connection, 1.25ms unit. Valid range is from
+ * 6 (7.5ms) to 3200 (4000ms).
+ * @param latency Worker latency for the connection in number of connection events. Valid range
+ * is from 0 to 499
+ * @param timeout Supervision timeout for this connection, in 10ms unit. Valid range is from 10
+ * (0.1s) to 3200 (32s)
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the connection has been updated
+ * successfully
+ * @hide
+ */
+ public void onConnectionUpdated(BluetoothDevice device, int interval, int latency, int timeout,
+ int status) {
+ }
+
+ /**
+ * Callback indicating the LE connection's subrate parameters were updated.
+ *
+ * @param device The remote device involved
+ * @param subrateFactor for the LE connection.
+ * @param latency for the LE connection in number of subrated connection events.
+ * Valid range is from 0 to 499.
+ * @param contNum Valid range is from 0 to 499.
+ * @param timeout Supervision timeout for this connection, in 10ms unit. Valid range is from 10
+ * (0.1s) to 3200 (32s)
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if LE connection subrating has been changed
+ * successfully.
+ * @hide
+ */
+ public void onSubrateChange(BluetoothDevice device, int subrateFactor, int latency, int contNum,
+ int timeout, int status) {}
+}
diff --git a/android-34/android/bluetooth/BluetoothGattService.java b/android-34/android/bluetooth/BluetoothGattService.java
new file mode 100644
index 0000000..ab9e791
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothGattService.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a Bluetooth GATT Service
+ *
+ * <p> Gatt Service contains a collection of {@link BluetoothGattCharacteristic},
+ * as well as referenced services.
+ */
+public class BluetoothGattService implements Parcelable {
+
+ /**
+ * Primary service
+ */
+ public static final int SERVICE_TYPE_PRIMARY = 0;
+
+ /**
+ * Secondary service (included by primary services)
+ */
+ public static final int SERVICE_TYPE_SECONDARY = 1;
+
+
+ /**
+ * The remote device this service is associated with.
+ * This applies to client applications only.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected BluetoothDevice mDevice;
+
+ /**
+ * The UUID of this service.
+ *
+ * @hide
+ */
+ protected UUID mUuid;
+
+ /**
+ * Instance ID for this service.
+ *
+ * @hide
+ */
+ protected int mInstanceId;
+
+ /**
+ * Handle counter override (for conformance testing).
+ *
+ * @hide
+ */
+ protected int mHandles = 0;
+
+ /**
+ * Service type (Primary/Secondary).
+ *
+ * @hide
+ */
+ protected int mServiceType;
+
+ /**
+ * List of characteristics included in this service.
+ */
+ protected List<BluetoothGattCharacteristic> mCharacteristics;
+
+ /**
+ * List of included services for this service.
+ */
+ protected List<BluetoothGattService> mIncludedServices;
+
+ /**
+ * Whether the service uuid should be advertised.
+ */
+ private boolean mAdvertisePreferred;
+
+ /**
+ * Create a new BluetoothGattService.
+ *
+ * @param uuid The UUID for this service
+ * @param serviceType The type of this service,
+ * {@link BluetoothGattService#SERVICE_TYPE_PRIMARY}
+ * or {@link BluetoothGattService#SERVICE_TYPE_SECONDARY}
+ */
+ public BluetoothGattService(UUID uuid, int serviceType) {
+ mDevice = null;
+ mUuid = uuid;
+ mInstanceId = 0;
+ mServiceType = serviceType;
+ mCharacteristics = new ArrayList<BluetoothGattCharacteristic>();
+ mIncludedServices = new ArrayList<BluetoothGattService>();
+ }
+
+ /**
+ * Create a new BluetoothGattService
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattService(BluetoothDevice device, UUID uuid,
+ int instanceId, int serviceType) {
+ mDevice = device;
+ mUuid = uuid;
+ mInstanceId = instanceId;
+ mServiceType = serviceType;
+ mCharacteristics = new ArrayList<BluetoothGattCharacteristic>();
+ mIncludedServices = new ArrayList<BluetoothGattService>();
+ }
+
+ /**
+ * Create a new BluetoothGattService
+ *
+ * @hide
+ */
+ public BluetoothGattService(UUID uuid, int instanceId, int serviceType) {
+ mDevice = null;
+ mUuid = uuid;
+ mInstanceId = instanceId;
+ mServiceType = serviceType;
+ mCharacteristics = new ArrayList<BluetoothGattCharacteristic>();
+ mIncludedServices = new ArrayList<BluetoothGattService>();
+ }
+
+ /**
+ * @hide
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeInt(mInstanceId);
+ out.writeInt(mServiceType);
+ out.writeTypedList(mCharacteristics);
+
+ ArrayList<BluetoothGattIncludedService> includedServices =
+ new ArrayList<BluetoothGattIncludedService>(mIncludedServices.size());
+ for (BluetoothGattService s : mIncludedServices) {
+ includedServices.add(new BluetoothGattIncludedService(s.getUuid(),
+ s.getInstanceId(), s.getType()));
+ }
+ out.writeTypedList(includedServices);
+ }
+
+ public static final @NonNull Creator<BluetoothGattService> CREATOR = new Creator<>() {
+ public BluetoothGattService createFromParcel(Parcel in) {
+ return new BluetoothGattService(in);
+ }
+
+ public BluetoothGattService[] newArray(int size) {
+ return new BluetoothGattService[size];
+ }
+ };
+
+ private BluetoothGattService(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mInstanceId = in.readInt();
+ mServiceType = in.readInt();
+
+ mCharacteristics = new ArrayList<BluetoothGattCharacteristic>();
+
+ ArrayList<BluetoothGattCharacteristic> chrcs =
+ in.createTypedArrayList(BluetoothGattCharacteristic.CREATOR);
+ if (chrcs != null) {
+ for (BluetoothGattCharacteristic chrc : chrcs) {
+ chrc.setService(this);
+ mCharacteristics.add(chrc);
+ }
+ }
+
+ mIncludedServices = new ArrayList<BluetoothGattService>();
+
+ ArrayList<BluetoothGattIncludedService> inclSvcs =
+ in.createTypedArrayList(BluetoothGattIncludedService.CREATOR);
+ if (chrcs != null) {
+ for (BluetoothGattIncludedService isvc : inclSvcs) {
+ mIncludedServices.add(new BluetoothGattService(null, isvc.getUuid(),
+ isvc.getInstanceId(), isvc.getType()));
+ }
+ }
+ }
+
+ /**
+ * Returns the device associated with this service.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Returns the device associated with this service.
+ *
+ * @hide
+ */
+ /*package*/ void setDevice(BluetoothDevice device) {
+ mDevice = device;
+ }
+
+ /**
+ * Add an included service to this service.
+ *
+ * @param service The service to be added
+ * @return true, if the included service was added to the service
+ */
+ @RequiresLegacyBluetoothPermission
+ public boolean addService(BluetoothGattService service) {
+ mIncludedServices.add(service);
+ return true;
+ }
+
+ /**
+ * Add a characteristic to this service.
+ *
+ * @param characteristic The characteristics to be added
+ * @return true, if the characteristic was added to the service
+ */
+ @RequiresLegacyBluetoothPermission
+ public boolean addCharacteristic(BluetoothGattCharacteristic characteristic) {
+ mCharacteristics.add(characteristic);
+ characteristic.setService(this);
+ return true;
+ }
+
+ /**
+ * Get characteristic by UUID and instanceId.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattCharacteristic getCharacteristic(UUID uuid, int instanceId) {
+ for (BluetoothGattCharacteristic characteristic : mCharacteristics) {
+ if (uuid.equals(characteristic.getUuid())
+ && characteristic.getInstanceId() == instanceId) {
+ return characteristic;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Force the instance ID.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setInstanceId(int instanceId) {
+ mInstanceId = instanceId;
+ }
+
+ /**
+ * Get the handle count override (conformance testing.
+ *
+ * @hide
+ */
+ /*package*/ int getHandles() {
+ return mHandles;
+ }
+
+ /**
+ * Force the number of handles to reserve for this service.
+ * This is needed for conformance testing only.
+ *
+ * @hide
+ */
+ public void setHandles(int handles) {
+ mHandles = handles;
+ }
+
+ /**
+ * Add an included service to the internal map.
+ *
+ * @hide
+ */
+ public void addIncludedService(BluetoothGattService includedService) {
+ mIncludedServices.add(includedService);
+ }
+
+ /**
+ * Returns the UUID of this service
+ *
+ * @return UUID of this service
+ */
+ public UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns the instance ID for this service
+ *
+ * <p>If a remote device offers multiple services with the same UUID
+ * (ex. multiple battery services for different batteries), the instance
+ * ID is used to distuinguish services.
+ *
+ * @return Instance ID of this service
+ */
+ public int getInstanceId() {
+ return mInstanceId;
+ }
+
+ /**
+ * Get the type of this service (primary/secondary)
+ */
+ public int getType() {
+ return mServiceType;
+ }
+
+ /**
+ * Get the list of included GATT services for this service.
+ *
+ * @return List of included services or empty list if no included services were discovered.
+ */
+ public List<BluetoothGattService> getIncludedServices() {
+ return mIncludedServices;
+ }
+
+ /**
+ * Returns a list of characteristics included in this service.
+ *
+ * @return Characteristics included in this service
+ */
+ public List<BluetoothGattCharacteristic> getCharacteristics() {
+ return mCharacteristics;
+ }
+
+ /**
+ * Returns a characteristic with a given UUID out of the list of
+ * characteristics offered by this service.
+ *
+ * <p>This is a convenience function to allow access to a given characteristic
+ * without enumerating over the list returned by {@link #getCharacteristics}
+ * manually.
+ *
+ * <p>If a remote service offers multiple characteristics with the same
+ * UUID, the first instance of a characteristic with the given UUID
+ * is returned.
+ *
+ * @return GATT characteristic object or null if no characteristic with the given UUID was
+ * found.
+ */
+ public BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
+ for (BluetoothGattCharacteristic characteristic : mCharacteristics) {
+ if (uuid.equals(characteristic.getUuid())) {
+ return characteristic;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the uuid of the service should be advertised.
+ *
+ * @hide
+ */
+ public boolean isAdvertisePreferred() {
+ return mAdvertisePreferred;
+ }
+
+ /**
+ * Set whether the service uuid should be advertised.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void setAdvertisePreferred(boolean advertisePreferred) {
+ mAdvertisePreferred = advertisePreferred;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothHapClient.java b/android-34/android/bluetooth/BluetoothHapClient.java
new file mode 100644
index 0000000..b337edd
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHapClient.java
@@ -0,0 +1,1408 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides a public APIs to control the Bluetooth Hearing Access Profile client service.
+ *
+ * <p>BluetoothHapClient is a proxy object for controlling the Bluetooth HAP
+ * Service client via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the
+ * BluetoothHapClient proxy object.
+ * @hide
+ */
+@SystemApi
+public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable {
+ private static final String TAG = "BluetoothHapClient";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>();
+
+ private CloseGuard mCloseGuard;
+
+ private final class HapClientServiceListener extends ForwardingServiceListener {
+ HapClientServiceListener(ServiceListener listener) {
+ super(listener);
+ }
+
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ try {
+ if (profile == HAP_CLIENT) {
+ // re-register the service-to-app callback
+ synchronized (mCallbackExecutorMap) {
+ if (mCallbackExecutorMap.isEmpty()) {
+ return;
+ }
+
+ try {
+ final IBluetoothHapClient service = getService();
+ if (service != null) {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ service.registerCallback(mCallback, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n"
+ + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ } finally {
+ super.onServiceConnected(profile, proxy);
+ }
+ }
+ }
+
+ /**
+ * This class provides callbacks mechanism for the BluetoothHapClient profile.
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface Callback {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ // needed for future release compatibility
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST,
+ BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
+ BluetoothStatusCodes.REASON_REMOTE_REQUEST,
+ BluetoothStatusCodes.REASON_SYSTEM_POLICY,
+ })
+ @interface PresetSelectionReason {}
+
+ /**
+ * Invoked to inform about HA device's currently active preset.
+ *
+ * @param device remote device,
+ * @param presetIndex the currently active preset index.
+ * @param reason reason for the selected preset change
+ *
+ * @hide
+ */
+ @SystemApi
+ void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex,
+ @PresetSelectionReason int reason);
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ // needed for future release compatibility
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
+ BluetoothStatusCodes.REASON_SYSTEM_POLICY,
+ BluetoothStatusCodes.ERROR_REMOTE_OPERATION_REJECTED,
+ BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
+ BluetoothStatusCodes.ERROR_HAP_INVALID_PRESET_INDEX,
+ })
+ @interface PresetSelectionFailureReason {}
+
+ /**
+ * Invoked inform about the result of a failed preset change attempt.
+ *
+ * @param device remote device,
+ * @param reason failure reason.
+ *
+ * @hide
+ */
+ @SystemApi
+ void onPresetSelectionFailed(@NonNull BluetoothDevice device,
+ @PresetSelectionFailureReason int reason);
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ // needed for future release compatibility
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
+ BluetoothStatusCodes.REASON_SYSTEM_POLICY,
+ BluetoothStatusCodes.ERROR_REMOTE_OPERATION_REJECTED,
+ BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
+ BluetoothStatusCodes.ERROR_HAP_INVALID_PRESET_INDEX,
+ BluetoothStatusCodes.ERROR_CSIP_INVALID_GROUP_ID,
+ })
+ @interface GroupPresetSelectionFailureReason {}
+
+ /**
+ * Invoked to inform about the result of a failed preset change attempt.
+ *
+ * The implementation will try to restore the state for every device back to original
+ *
+ * @param hapGroupId valid HAP group ID,
+ * @param reason failure reason.
+ *
+ * @hide
+ */
+ @SystemApi
+ void onPresetSelectionForGroupFailed(int hapGroupId,
+ @GroupPresetSelectionFailureReason int reason);
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ // needed for future release compatibility
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST,
+ BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
+ BluetoothStatusCodes.REASON_REMOTE_REQUEST,
+ BluetoothStatusCodes.REASON_SYSTEM_POLICY,
+ })
+ @interface PresetInfoChangeReason {}
+
+ /**
+ * Invoked to inform about the preset list changes.
+ *
+ * @param device remote device,
+ * @param presetInfoList a list of all preset information on the target device
+ * @param reason reason for the preset list change
+ *
+ * @hide
+ */
+ @SystemApi
+ void onPresetInfoChanged(@NonNull BluetoothDevice device,
+ @NonNull List<BluetoothHapPresetInfo> presetInfoList,
+ @PresetInfoChangeReason int reason);
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ // needed for future release compatibility
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
+ BluetoothStatusCodes.REASON_SYSTEM_POLICY,
+ BluetoothStatusCodes.ERROR_REMOTE_OPERATION_REJECTED,
+ BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
+ BluetoothStatusCodes.ERROR_HAP_PRESET_NAME_TOO_LONG,
+ BluetoothStatusCodes.ERROR_HAP_INVALID_PRESET_INDEX,
+ })
+ @interface PresetNameChangeFailureReason {}
+
+ /**
+ * Invoked to inform about the failed preset rename attempt.
+ *
+ * @param device remote device
+ * @param reason Failure reason code.
+ * @hide
+ */
+ @SystemApi
+ void onSetPresetNameFailed(@NonNull BluetoothDevice device,
+ @PresetNameChangeFailureReason int reason);
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ // needed for future release compatibility
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
+ BluetoothStatusCodes.REASON_SYSTEM_POLICY,
+ BluetoothStatusCodes.ERROR_REMOTE_OPERATION_REJECTED,
+ BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
+ BluetoothStatusCodes.ERROR_HAP_PRESET_NAME_TOO_LONG,
+ BluetoothStatusCodes.ERROR_HAP_INVALID_PRESET_INDEX,
+ BluetoothStatusCodes.ERROR_CSIP_INVALID_GROUP_ID,
+ })
+ @interface GroupPresetNameChangeFailureReason {}
+
+ /**
+ * Invoked to inform about the failed preset rename attempt.
+ *
+ * The implementation will try to restore the state for every device back to original
+ *
+ * @param hapGroupId valid HAP group ID,
+ * @param reason Failure reason code.
+ * @hide
+ */
+ @SystemApi
+ void onSetPresetNameForGroupFailed(int hapGroupId,
+ @GroupPresetNameChangeFailureReason int reason);
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothHapClientCallback mCallback = new IBluetoothHapClientCallback.Stub() {
+ @Override
+ public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex,
+ int reasonCode) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry:
+ mCallbackExecutorMap.entrySet()) {
+ BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onPresetSelected(device, presetIndex, reasonCode));
+ }
+ }
+
+ @Override
+ public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int status) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry:
+ mCallbackExecutorMap.entrySet()) {
+ BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onPresetSelectionFailed(device, status));
+ }
+ }
+
+ @Override
+ public void onPresetSelectionForGroupFailed(int hapGroupId, int statusCode) {
+ for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry:
+ mCallbackExecutorMap.entrySet()) {
+ BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(
+ () -> callback.onPresetSelectionForGroupFailed(hapGroupId, statusCode));
+ }
+ }
+
+ @Override
+ public void onPresetInfoChanged(@NonNull BluetoothDevice device,
+ @NonNull List<BluetoothHapPresetInfo> presetInfoList, int statusCode) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry:
+ mCallbackExecutorMap.entrySet()) {
+ BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(
+ () -> callback.onPresetInfoChanged(device, presetInfoList, statusCode));
+ }
+ }
+
+ @Override
+ public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int status) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry:
+ mCallbackExecutorMap.entrySet()) {
+ BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onSetPresetNameFailed(device, status));
+ }
+ }
+
+ @Override
+ public void onSetPresetNameForGroupFailed(int hapGroupId, int status) {
+ for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry:
+ mCallbackExecutorMap.entrySet()) {
+ BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onSetPresetNameForGroupFailed(hapGroupId, status));
+ }
+ }
+ };
+
+ /**
+ * Intent used to broadcast the change in connection state of the Hearing Access Profile Client
+ * service. Please note that in the binaural case, there will be two different LE devices for
+ * the left and right side and each device will have their own connection state changes.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the device availability change and the availability of its
+ * presets. Please note that in the binaural case, there will be two different LE devices for
+ * the left and right side and each device will have their own availability event.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link #EXTRA_HAP_FEATURES} - Supported features map. </li>
+ * </ul>
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_DEVICE_AVAILABLE =
+ "android.bluetooth.action.HAP_DEVICE_AVAILABLE";
+
+ /**
+ * Contains a list of all available presets
+ * @hide
+ */
+ public static final String EXTRA_HAP_FEATURES = "android.bluetooth.extra.HAP_FEATURES";
+
+ /**
+ * Represents an invalid index value. This is usually value returned in a currently
+ * active preset request for a device which is not connected. This value shouldn't be used
+ * in the API calls.
+ * @hide
+ */
+ public static final int PRESET_INDEX_UNAVAILABLE = IBluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
+
+ /**
+ * Hearing aid type value. Indicates this Bluetooth device is belongs to a binaural hearing aid
+ * set. A binaural hearing aid set is two hearing aids that form a Coordinated Set, one for the
+ * right ear and one for the left ear of the user. Typically used by a user with bilateral
+ * hearing loss.
+ * @hide
+ */
+ @SystemApi
+ public static final int TYPE_BINAURAL = 0b00;
+
+ /**
+ * Hearing aid type value. Indicates this Bluetooth device is a single hearing aid for the left
+ * or the right ear. Typically used by a user with unilateral hearing loss.
+ * @hide
+ */
+ @SystemApi
+ public static final int TYPE_MONAURAL = 0b01;
+
+ /**
+ * Hearing aid type value. Indicates this Bluetooth device is two hearing aids with a connection
+ * to one another that expose a single Bluetooth radio interface.
+ * @hide
+ */
+ @SystemApi
+ public static final int TYPE_BANDED = 0b10;
+
+ /**
+ * Hearing aid type value. This value is reserved for future use.
+ * @hide
+ */
+ @SystemApi
+ public static final int TYPE_RFU = 0b11;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ TYPE_BINAURAL,
+ TYPE_MONAURAL,
+ TYPE_BANDED,
+ TYPE_RFU,
+ })
+ @interface HearingAidType {}
+
+ /**
+ * Feature mask value.
+ * @hide
+ */
+ public static final int FEATURE_HEARING_AID_TYPE_MASK = 0b11;
+
+ /**
+ * Feature mask value.
+ * @hide
+ */
+ public static final int FEATURE_SYNCHRONIZATED_PRESETS_MASK =
+ 1 << IBluetoothHapClient.FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS;
+
+ /**
+ * Feature mask value.
+ * @hide
+ */
+ public static final int FEATURE_INDEPENDENT_PRESETS_MASK =
+ 1 << IBluetoothHapClient.FEATURE_BIT_NUM_INDEPENDENT_PRESETS;
+
+ /**
+ * Feature mask value.
+ * @hide
+ */
+ public static final int FEATURE_DYNAMIC_PRESETS_MASK =
+ 1 << IBluetoothHapClient.FEATURE_BIT_NUM_DYNAMIC_PRESETS;
+
+ /**
+ * Feature mask value.
+ * @hide
+ */
+ public static final int FEATURE_WRITABLE_PRESETS_MASK =
+ 1 << IBluetoothHapClient.FEATURE_BIT_NUM_WRITABLE_PRESETS;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ FEATURE_HEARING_AID_TYPE_MASK,
+ FEATURE_SYNCHRONIZATED_PRESETS_MASK,
+ FEATURE_INDEPENDENT_PRESETS_MASK,
+ FEATURE_DYNAMIC_PRESETS_MASK,
+ FEATURE_WRITABLE_PRESETS_MASK,
+ })
+ @interface FeatureMask {}
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHapClient> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HAP_CLIENT, "BluetoothHapClient",
+ IBluetoothHapClient.class.getName()) {
+ @Override
+ public IBluetoothHapClient getServiceInterface(IBinder service) {
+ return IBluetoothHapClient.Stub.asInterface(service);
+ }
+ };
+
+
+ /**
+ * Create a BluetoothHapClient proxy object for interacting with the local
+ * Bluetooth Hearing Access Profile (HAP) client.
+ */
+ /*package*/ BluetoothHapClient(Context context, ServiceListener listener) {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mAttributionSource = mAdapter.getAttributionSource();
+ mProfileConnector.connect(context, new HapClientServiceListener(listener));
+
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * @hide
+ */
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ if (VDBG) log("close()");
+
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothHapClient getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Register a {@link Callback} that will be invoked during the
+ * operation of this profile.
+ *
+ * Repeated registration of the same <var>callback</var> object after the first call to this
+ * method will result with IllegalArgumentException being thrown, even when the
+ * <var>executor</var> is different. API caller would have to call
+ * {@link #unregisterCallback(Callback)} with the same callback object before registering it
+ * again.
+ *
+ * @param executor an {@link Executor} to execute given callback
+ * @param callback user implementation of the {@link Callback}
+ * @throws NullPointerException if a null executor, or callback is given, or
+ * IllegalArgumentException if the same <var>callback<var> is already registered.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void registerCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Callback callback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ if (DBG) log("registerCallback");
+
+ synchronized (mCallbackExecutorMap) {
+ // If the callback map is empty, we register the service-to-app callback
+ if (mCallbackExecutorMap.isEmpty()) {
+ if (!isEnabled()) {
+ /* If Bluetooth is off, just store callback and it will be registered
+ * when Bluetooth is on
+ */
+ mCallbackExecutorMap.put(callback, executor);
+ return;
+ }
+ try {
+ final IBluetoothHapClient service = getService();
+ if (service != null) {
+ final SynchronousResultReceiver<Integer> recv =
+ SynchronousResultReceiver.get();
+ service.registerCallback(mCallback, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ // Adds the passed in callback to our map of callbacks to executors
+ if (mCallbackExecutorMap.containsKey(callback)) {
+ throw new IllegalArgumentException("This callback has already been registered");
+ }
+ mCallbackExecutorMap.put(callback, executor);
+ }
+ }
+
+ /**
+ * Unregister the specified {@link Callback}.
+ * <p>The same {@link Callback} object used when calling
+ * {@link #registerCallback(Executor, Callback)} must be used.
+ *
+ * <p>Callbacks are automatically unregistered when application process goes away
+ *
+ * @param callback user implementation of the {@link Callback}
+ * @throws NullPointerException when callback is null or IllegalArgumentException when no
+ * callback is registered
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void unregisterCallback(@NonNull Callback callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ if (DBG) log("unregisterCallback");
+
+ synchronized (mCallbackExecutorMap) {
+ if (mCallbackExecutorMap.remove(callback) == null) {
+ throw new IllegalArgumentException("This callback has not been registered");
+ }
+ }
+
+ // If the callback map is empty, we unregister the service-to-app callback
+ if (mCallbackExecutorMap.isEmpty()) {
+ try {
+ final IBluetoothHapClient service = getService();
+ if (service != null) {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.unregisterCallback(mCallback, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ }
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return {@code true} if connectionPolicy is set, {@code false} on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ Objects.requireNonNull(device, "BluetoothDevice cannot be null");
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled() && isValidDevice(device)
+ && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device or {@link #CONNECTION_POLICY_FORBIDDEN} if device is
+ * null
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothHapClient service = getService();
+ final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionPolicy(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @Override
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) Log.d(TAG, "getConnectedDevices()");
+ final IBluetoothHapClient service = getService();
+ final List defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List> recv = SynchronousResultReceiver.get();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @Override
+ @NonNull
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
+ if (VDBG) Log.d(TAG, "getDevicesMatchingConnectionStates()");
+ final IBluetoothHapClient service = getService();
+ final List defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List> recv = SynchronousResultReceiver.get();
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @Override
+ @BluetoothProfile.BtProfileState
+ public int getConnectionState(@NonNull BluetoothDevice device) {
+ if (VDBG) Log.d(TAG, "getConnectionState(" + device + ")");
+ final IBluetoothHapClient service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the group identifier, which can be used in the group related part of
+ * the API.
+ *
+ * <p>Users are expected to get group identifier for each of the connected
+ * device to discover the device grouping. This allows them to make an informed
+ * decision which devices can be controlled by single group API call and which
+ * require individual device calls.
+ *
+ * <p>Note that some binaural HA devices may not support group operations,
+ * therefore are not considered a valid HAP group. In such case -1 is returned
+ * even if such device is a valid Le Audio Coordinated Set member.
+ *
+ * @param device
+ * @return valid group identifier or -1
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public int getHapGroup(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final int defaultValue = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getHapGroup(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the currently active preset for a HA device.
+ *
+ * @param device is the device for which we want to set the active preset
+ * @return active preset index
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public int getActivePresetIndex(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final int defaultValue = PRESET_INDEX_UNAVAILABLE;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getActivePresetIndex(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the currently active preset info for a remote device.
+ *
+ * @param device is the device for which we want to get the preset name
+ * @return currently active preset info if selected, null if preset info is not available
+ * for the remote device
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public @Nullable BluetoothHapPresetInfo getActivePresetInfo(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final BluetoothHapPresetInfo defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<BluetoothHapPresetInfo> recv =
+ SynchronousResultReceiver.get();
+ service.getActivePresetInfo(device, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * Selects the currently active preset for a HA device
+ *
+ * On success, {@link Callback#onPresetSelected(BluetoothDevice, int, int)} will be called with
+ * reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}
+ * On failure, {@link Callback#onPresetSelectionFailed(BluetoothDevice, int)} will be called.
+ *
+ * @param device is the device for which we want to set the active preset
+ * @param presetIndex is an index of one of the available presets
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public void selectPreset(@NonNull BluetoothDevice device, int presetIndex) {
+ final IBluetoothHapClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ service.selectPreset(device, presetIndex, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Selects the currently active preset for a Hearing Aid device group.
+ *
+ * <p> This group call may replace multiple device calls if those are part of the
+ * valid HAS group. Note that binaural HA devices may or may not support group.
+ *
+ * On success, {@link Callback#onPresetSelected(BluetoothDevice, int, int)} will be called
+ * for each device within the group with reason code
+ * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}
+ * On failure, {@link Callback#onPresetSelectionForGroupFailed(int, int)} will be
+ * called for the group.
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @param presetIndex is an index of one of the available presets
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public void selectPresetForGroup(int groupId, int presetIndex) {
+ final IBluetoothHapClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.selectPresetForGroup(groupId, presetIndex, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Sets the next preset as a currently active preset for a HA device
+ *
+ * <p> Note that the meaning of 'next' is HA device implementation specific and
+ * does not necessarily mean a higher preset index.
+ *
+ * @param device is the device for which we want to set the active preset
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public void switchToNextPreset(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ service.switchToNextPreset(device, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Sets the next preset as a currently active preset for a HA device group
+ *
+ * <p> Note that the meaning of 'next' is HA device implementation specific and
+ * does not necessarily mean a higher preset index.
+ * <p> This group call may replace multiple device calls if those are part of the
+ * valid HAS group. Note that binaural HA devices may or may not support group.
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public void switchToNextPresetForGroup(int groupId) {
+ final IBluetoothHapClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.switchToNextPresetForGroup(groupId, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Sets the previous preset as a currently active preset for a HA device.
+ *
+ * <p> Note that the meaning of 'previous' is HA device implementation specific and
+ * does not necessarily mean a lower preset index.
+ *
+ * @param device is the device for which we want to set the active preset
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public void switchToPreviousPreset(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ service.switchToPreviousPreset(device, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Sets the previous preset as a currently active preset for a HA device group
+ *
+ * <p> Note the meaning of 'previous' is HA device implementation specific and
+ * does not necessarily mean a lower preset index.
+ * <p> This group call may replace multiple device calls if those are part of the
+ * valid HAS group. Note that binaural HA devices may or may not support group.
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public void switchToPreviousPresetForGroup(int groupId) {
+ final IBluetoothHapClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.switchToPreviousPresetForGroup(groupId, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Requests the preset info
+ *
+ * @param device is the device for which we want to get the preset name
+ * @param presetIndex is an index of one of the available presets
+ * @return preset info
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ @Nullable
+ public BluetoothHapPresetInfo getPresetInfo(@NonNull BluetoothDevice device, int presetIndex) {
+ final IBluetoothHapClient service = getService();
+ final BluetoothHapPresetInfo defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<BluetoothHapPresetInfo> recv =
+ SynchronousResultReceiver.get();
+ service.getPresetInfo(device, presetIndex, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get all preset info for a particular device
+ *
+ * @param device is the device for which we want to get all presets info
+ * @return a list of all known preset info
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public @NonNull List<BluetoothHapPresetInfo> getAllPresetInfo(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final List<BluetoothHapPresetInfo> defaultValue = new ArrayList<BluetoothHapPresetInfo>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothHapPresetInfo>> recv =
+ SynchronousResultReceiver.get();
+ service.getAllPresetInfo(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Requests HAP features
+ *
+ * @param device is the device for which we want to get features for
+ * @return features value with feature bits set
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public int getFeatures(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final int defaultValue = 0x00;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getFeatures(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Retrieves hearing aid type from feature value.
+ *
+ * @param device is the device for which we want to get the hearing aid type
+ * @return hearing aid type
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ @HearingAidType
+ public int getHearingAidType(@NonNull BluetoothDevice device) {
+ return getFeatures(device) & FEATURE_HEARING_AID_TYPE_MASK;
+ }
+
+ /**
+ * Retrieves if this device supports synchronized presets or not from feature value.
+ *
+ * @param device is the device for which we want to know if it supports synchronized presets
+ * @return {@code true} if the device supports synchronized presets, {@code false} otherwise
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public boolean supportsSynchronizedPresets(@NonNull BluetoothDevice device) {
+ return (getFeatures(device) & FEATURE_SYNCHRONIZATED_PRESETS_MASK)
+ == FEATURE_SYNCHRONIZATED_PRESETS_MASK;
+ }
+
+ /**
+ * Retrieves if this device supports independent presets or not from feature value.
+ *
+ * @param device is the device for which we want to know if it supports independent presets
+ * @return {@code true} if the device supports independent presets, {@code false} otherwise
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public boolean supportsIndependentPresets(@NonNull BluetoothDevice device) {
+ return (getFeatures(device) & FEATURE_INDEPENDENT_PRESETS_MASK)
+ == FEATURE_INDEPENDENT_PRESETS_MASK;
+ }
+
+ /**
+ * Retrieves if this device supports dynamic presets or not from feature value.
+ *
+ * @param device is the device for which we want to know if it supports dynamic presets
+ * @return {@code true} if the device supports dynamic presets, {@code false} otherwise
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public boolean supportsDynamicPresets(@NonNull BluetoothDevice device) {
+ return (getFeatures(device) & FEATURE_DYNAMIC_PRESETS_MASK)
+ == FEATURE_DYNAMIC_PRESETS_MASK;
+ }
+
+ /**
+ * Retrieves if this device supports writable presets or not from feature value.
+ *
+ * @param device is the device for which we want to know if it supports writable presets
+ * @return {@code true} if the device supports writable presets, {@code false} otherwise
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public boolean supportsWritablePresets(@NonNull BluetoothDevice device) {
+ return (getFeatures(device) & FEATURE_WRITABLE_PRESETS_MASK)
+ == FEATURE_WRITABLE_PRESETS_MASK;
+ }
+
+ /**
+ * Sets the preset name for a particular device
+ *
+ * <p> Note that the name length is restricted to 40 characters.
+ *
+ * On success, {@link Callback#onPresetInfoChanged(BluetoothDevice, List, int)}
+ * with a new name will be called and reason code
+ * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}
+ * On failure, {@link Callback#onSetPresetNameFailed(BluetoothDevice, int)} will be called.
+ *
+ * @param device is the device for which we want to get the preset name
+ * @param presetIndex is an index of one of the available presets
+ * @param name is a new name for a preset, maximum length is 40 characters
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public void setPresetName(@NonNull BluetoothDevice device, int presetIndex,
+ @NonNull String name) {
+ final IBluetoothHapClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ service.setPresetName(device, presetIndex, name, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Sets the name for a hearing aid preset.
+ *
+ * <p> Note that the name length is restricted to 40 characters.
+ *
+ * On success, {@link Callback#onPresetInfoChanged(BluetoothDevice, List, int)}
+ * with a new name will be called for each device within the group with reason code
+ * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}
+ * On failure, {@link Callback#onSetPresetNameForGroupFailed(int, int)} will be invoked
+ *
+ * @param groupId is the device group identifier
+ * @param presetIndex is an index of one of the available presets
+ * @param name is a new name for a preset, maximum length is 40 characters
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public void setPresetNameForGroup(int groupId, int presetIndex, @NonNull String name) {
+ final IBluetoothHapClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.setPresetNameForGroup(groupId, presetIndex, name, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ private boolean isEnabled() {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+ return false;
+ }
+
+ private boolean isValidDevice(BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothHapPresetInfo.java b/android-34/android/bluetooth/BluetoothHapPresetInfo.java
new file mode 100644
index 0000000..fc5c877
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHapPresetInfo.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Represents the Hearing Access Profile preset.
+ * @hide
+ */
+@SystemApi
+public final class BluetoothHapPresetInfo implements Parcelable {
+ private int mPresetIndex;
+ private String mPresetName = "";
+ private boolean mIsWritable;
+ private boolean mIsAvailable;
+
+ /**
+ * HapPresetInfo constructor
+ *
+ * @param presetIndex Preset index
+ * @param presetName Preset Name
+ * @param isWritable Is writable flag
+ * @param isAvailable Is available flag
+ */
+ /*package*/ BluetoothHapPresetInfo(int presetIndex, @NonNull String presetName,
+ boolean isWritable, boolean isAvailable) {
+ this.mPresetIndex = presetIndex;
+ this.mPresetName = presetName;
+ this.mIsWritable = isWritable;
+ this.mIsAvailable = isAvailable;
+ }
+
+ /**
+ * HapPresetInfo constructor
+ *
+ * @param in HapPresetInfo parcel
+ */
+ private BluetoothHapPresetInfo(@NonNull Parcel in) {
+ mPresetIndex = in.readInt();
+ mPresetName = in.readString();
+ mIsWritable = in.readBoolean();
+ mIsAvailable = in.readBoolean();
+ }
+
+ /**
+ * HapPresetInfo preset index
+ *
+ * @return Preset index
+ */
+ public int getIndex() {
+ return mPresetIndex;
+ }
+
+ /**
+ * HapPresetInfo preset name
+ *
+ * @return Preset name
+ */
+ public @NonNull String getName() {
+ return mPresetName;
+ }
+
+ /**
+ * HapPresetInfo preset writability
+ *
+ * @return If preset is writable
+ */
+ public boolean isWritable() {
+ return mIsWritable;
+ }
+
+ /**
+ * HapPresetInfo availability
+ *
+ * @return If preset is available
+ */
+ public boolean isAvailable() {
+ return mIsAvailable;
+ }
+
+ /**
+ * HapPresetInfo array creator
+ */
+ public static final @NonNull Creator<BluetoothHapPresetInfo> CREATOR =
+ new Creator<BluetoothHapPresetInfo>() {
+ public BluetoothHapPresetInfo createFromParcel(@NonNull Parcel in) {
+ return new BluetoothHapPresetInfo(in);
+ }
+
+ public BluetoothHapPresetInfo[] newArray(int size) {
+ return new BluetoothHapPresetInfo[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mPresetIndex);
+ dest.writeString(mPresetName);
+ dest.writeBoolean(mIsWritable);
+ dest.writeBoolean(mIsAvailable);
+ }
+
+ /**
+ * Builder for {@link BluetoothHapPresetInfo}.
+ * <p> By default, the preset index will be set to
+ * {@link BluetoothHapClient#PRESET_INDEX_UNAVAILABLE}, the name to an empty string,
+ * writability and availability both to false.
+ * @hide
+ */
+ public static final class Builder {
+ private int mPresetIndex = BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
+ private String mPresetName = "";
+ private boolean mIsWritable = false;
+ private boolean mIsAvailable = false;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param index The preset index for HAP preset info
+ * @param name The preset name for HAP preset info
+ */
+ public Builder(int index, @NonNull String name) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("The size of the preset name for HAP shall be at"
+ + " least one character long.");
+ }
+ if (index < 0) {
+ throw new IllegalArgumentException(
+ "Preset index for HAP shall be a non-negative value.");
+ }
+
+ mPresetIndex = index;
+ mPresetName = name;
+ }
+
+ /**
+ * Set preset writability for HAP preset info.
+ *
+ * @param isWritable whether preset is writable
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setWritable(boolean isWritable) {
+ mIsWritable = isWritable;
+ return this;
+ }
+
+ /**
+ * Set preset availability for HAP preset info.
+ *
+ * @param isAvailable whether preset is currently available to select
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setAvailable(boolean isAvailable) {
+ mIsAvailable = isAvailable;
+ return this;
+ }
+
+ /**
+ * Build {@link BluetoothHapPresetInfo}.
+ * @return new BluetoothHapPresetInfo built
+ */
+ public @NonNull BluetoothHapPresetInfo build() {
+ return new BluetoothHapPresetInfo(mPresetIndex, mPresetName, mIsWritable, mIsAvailable);
+ }
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothHeadset.java b/android-34/android/bluetooth/BluetoothHeadset.java
new file mode 100644
index 0000000..4241fff
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHeadset.java
@@ -0,0 +1,1433 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Public API for controlling the Bluetooth Headset Service. This includes both
+ * Bluetooth Headset and Handsfree (v1.5) profiles.
+ *
+ * <p>BluetoothHeadset is a proxy object for controlling the Bluetooth Headset
+ * Service via IPC.
+ *
+ * <p> Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothHeadset proxy object. Use
+ * {@link BluetoothAdapter#closeProfileProxy} to close the service connection.
+ *
+ * <p> Android only supports one connected Bluetooth Headset at a time.
+ * Each method is protected with its appropriate permission.
+ */
+public final class BluetoothHeadset implements BluetoothProfile {
+ private static final String TAG = "BluetoothHeadset";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the Headset
+ * profile.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in the Audio Connection state of the
+ * HFP profile.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_AUDIO_CONNECTED}, {@link #STATE_AUDIO_DISCONNECTED},
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AUDIO_STATE_CHANGED =
+ "android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the selection of a connected device as active.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED";
+
+ /**
+ * Intent used to broadcast that the headset has posted a
+ * vendor-specific event.
+ *
+ * <p>This intent will have 4 extras and 1 category.
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote Bluetooth Device
+ * </li>
+ * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD} - The vendor
+ * specific command </li>
+ * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} - The AT
+ * command type which can be one of {@link #AT_CMD_TYPE_READ},
+ * {@link #AT_CMD_TYPE_TEST}, or {@link #AT_CMD_TYPE_SET},
+ * {@link #AT_CMD_TYPE_BASIC},{@link #AT_CMD_TYPE_ACTION}. </li>
+ * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS} - Command
+ * arguments. </li>
+ * </ul>
+ *
+ * <p> The category is the Company ID of the vendor defining the
+ * vendor-specific command. {@link BluetoothAssignedNumbers}
+ *
+ * For example, for Plantronics specific events
+ * Category will be {@link #VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY}.55
+ *
+ * <p> For example, an AT+XEVENT=foo,3 will get translated into
+ * <ul>
+ * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD = +XEVENT </li>
+ * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE = AT_CMD_TYPE_SET </li>
+ * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS = foo, 3 </li>
+ * </ul>
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_VENDOR_SPECIFIC_HEADSET_EVENT =
+ "android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT";
+
+ /**
+ * A String extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT}
+ * intents that contains the name of the vendor-specific command.
+ */
+ public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD =
+ "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD";
+
+ /**
+ * An int extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT}
+ * intents that contains the AT command type of the vendor-specific command.
+ */
+ public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE =
+ "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE";
+
+ /**
+ * AT command type READ used with
+ * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
+ * For example, AT+VGM?. There are no arguments for this command type.
+ */
+ public static final int AT_CMD_TYPE_READ = 0;
+
+ /**
+ * AT command type TEST used with
+ * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
+ * For example, AT+VGM=?. There are no arguments for this command type.
+ */
+ public static final int AT_CMD_TYPE_TEST = 1;
+
+ /**
+ * AT command type SET used with
+ * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
+ * For example, AT+VGM=<args>.
+ */
+ public static final int AT_CMD_TYPE_SET = 2;
+
+ /**
+ * AT command type BASIC used with
+ * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
+ * For example, ATD. Single character commands and everything following the
+ * character are arguments.
+ */
+ public static final int AT_CMD_TYPE_BASIC = 3;
+
+ /**
+ * AT command type ACTION used with
+ * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
+ * For example, AT+CHUP. There are no arguments for action commands.
+ */
+ public static final int AT_CMD_TYPE_ACTION = 4;
+
+ /**
+ * A Parcelable String array extra field in
+ * {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intents that contains
+ * the arguments to the vendor-specific command.
+ */
+ public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS =
+ "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_ARGS";
+
+ /**
+ * The intent category to be used with {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT}
+ * for the companyId
+ */
+ public static final String VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY =
+ "android.bluetooth.headset.intent.category.companyid";
+
+ /**
+ * A vendor-specific command for unsolicited result code.
+ */
+ public static final String VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID";
+
+ /**
+ * A vendor-specific AT command
+ *
+ * @hide
+ */
+ public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XAPL = "+XAPL";
+
+ /**
+ * A vendor-specific AT command
+ *
+ * @hide
+ */
+ public static final String VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV = "+IPHONEACCEV";
+
+ /**
+ * Battery level indicator associated with
+ * {@link #VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV}
+ *
+ * @hide
+ */
+ public static final int VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL = 1;
+
+ /**
+ * A vendor-specific AT command
+ *
+ * @hide
+ */
+ public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT = "+XEVENT";
+
+ /**
+ * Battery level indicator associated with {@link #VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT}
+ *
+ * @hide
+ */
+ public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT_BATTERY_LEVEL = "BATTERY";
+
+ /**
+ * Headset state when SCO audio is not connected.
+ * This state can be one of
+ * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+ * {@link #ACTION_AUDIO_STATE_CHANGED} intent.
+ */
+ public static final int STATE_AUDIO_DISCONNECTED = 10;
+
+ /**
+ * Headset state when SCO audio is connecting.
+ * This state can be one of
+ * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+ * {@link #ACTION_AUDIO_STATE_CHANGED} intent.
+ */
+ public static final int STATE_AUDIO_CONNECTING = 11;
+
+ /**
+ * Headset state when SCO audio is connected.
+ * This state can be one of
+ * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+ * {@link #ACTION_AUDIO_STATE_CHANGED} intent.
+ */
+ public static final int STATE_AUDIO_CONNECTED = 12;
+
+ /**
+ * Intent used to broadcast the headset's indicator status
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_HF_INDICATORS_IND_ID} - The Assigned number of headset Indicator which
+ * is supported by the headset ( as indicated by AT+BIND command in the SLC
+ * sequence) or whose value is changed (indicated by AT+BIEV command) </li>
+ * <li> {@link #EXTRA_HF_INDICATORS_IND_VALUE} - Updated value of headset indicator. </li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - Remote device. </li>
+ * </ul>
+ * <p>{@link #EXTRA_HF_INDICATORS_IND_ID} is defined by Bluetooth SIG and each of the indicators
+ * are given an assigned number. Below shows the assigned number of Indicator added so far
+ * - Enhanced Safety - 1, Valid Values: 0 - Disabled, 1 - Enabled
+ * - Battery Level - 2, Valid Values: 0~100 - Remaining level of Battery
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HF_INDICATORS_VALUE_CHANGED =
+ "android.bluetooth.headset.action.HF_INDICATORS_VALUE_CHANGED";
+
+ /**
+ * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED}
+ * intents that contains the assigned number of the headset indicator as defined by
+ * Bluetooth SIG that is being sent. Value range is 0-65535 as defined in HFP 1.7
+ *
+ * @hide
+ */
+ public static final String EXTRA_HF_INDICATORS_IND_ID =
+ "android.bluetooth.headset.extra.HF_INDICATORS_IND_ID";
+
+ /**
+ * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED}
+ * intents that contains the value of the Headset indicator that is being sent.
+ *
+ * @hide
+ */
+ public static final String EXTRA_HF_INDICATORS_IND_VALUE =
+ "android.bluetooth.headset.extra.HF_INDICATORS_IND_VALUE";
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHeadset> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HEADSET, "BluetoothHeadset",
+ IBluetoothHeadset.class.getName()) {
+ @Override
+ public IBluetoothHeadset getServiceInterface(IBinder service) {
+ return IBluetoothHeadset.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothHeadset proxy object.
+ */
+ /* package */ BluetoothHeadset(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /**
+ * Close the connection to the backing service. Other public functions of BluetoothHeadset will
+ * return default error results once close() has been called. Multiple invocations of close()
+ * are ok.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothHeadset getService() {
+ return mProfileConnector.getService();
+ }
+
+ /** {@hide} */
+ @Override
+ protected void finalize() throws Throwable {
+ // The empty finalize needs to be kept or the
+ // cts signature tests would fail.
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <p> Currently, the system supports only 1 connection to the
+ * headset/handsfree profile. The API will automatically disconnect connected
+ * devices before connecting.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is already connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that
+ * connection state intent for the profile will be broadcasted with
+ * the state. Users can get the connection state of the profile
+ * from this intent.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.connect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Initiate disconnection from a profile
+ *
+ * <p> This API will return false in scenarios like the profile on the
+ * Bluetooth device is not in connected state etc. When this API returns,
+ * true, it is guaranteed that the connection state change
+ * intent will be broadcasted with the state. Users can get the
+ * disconnection state of the profile from this intent.
+ *
+ * <p> If the disconnection is initiated by a remote device, the state
+ * will transition from {@link #STATE_CONNECTED} to
+ * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
+ * host (local) device the state will transition from
+ * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
+ * state {@link #STATE_DISCONNECTED}. The transition to
+ * {@link #STATE_DISCONNECTING} can be used to distinguish between the
+ * two scenarios.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.disconnect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothHeadset service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothHeadset service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (VDBG) log("getConnectionState(" + device + ")");
+ final IBluetoothHeadset service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true if connectionPolicy is set, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)
+ && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the priority of the profile.
+ *
+ * <p> The priority can be any of:
+ * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF},
+ * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getPriority(BluetoothDevice device) {
+ if (VDBG) log("getPriority(" + device + ")");
+ return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothHeadset service = getService();
+ final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionPolicy(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Checks whether the headset supports some form of noise reduction
+ *
+ * @param device Bluetooth device
+ * @return true if echo cancellation and/or noise reduction is supported, false otherwise
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isNoiseReductionSupported(@NonNull BluetoothDevice device) {
+ if (DBG) log("isNoiseReductionSupported()");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.isNoiseReductionSupported(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Checks whether the headset supports voice recognition
+ *
+ * @param device Bluetooth device
+ * @return true if voice recognition is supported, false otherwise
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isVoiceRecognitionSupported(@NonNull BluetoothDevice device) {
+ if (DBG) log("isVoiceRecognitionSupported()");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.isVoiceRecognitionSupported(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Start Bluetooth voice recognition. This methods sends the voice
+ * recognition AT command to the headset and establishes the
+ * audio connection.
+ *
+ * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
+ * If this function returns true, this intent will be broadcasted with
+ * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}.
+ *
+ * <p> {@link #EXTRA_STATE} will transition from
+ * {@link #STATE_AUDIO_CONNECTING} to {@link #STATE_AUDIO_CONNECTED} when
+ * audio connection is established and to {@link #STATE_AUDIO_DISCONNECTED}
+ * in case of failure to establish the audio connection.
+ *
+ * @param device Bluetooth headset
+ * @return false if there is no headset connected, or the connected headset doesn't support
+ * voice recognition, or voice recognition is already started, or audio channel is occupied,
+ * or on error, true otherwise
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public boolean startVoiceRecognition(BluetoothDevice device) {
+ if (DBG) log("startVoiceRecognition()");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.startVoiceRecognition(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Stop Bluetooth Voice Recognition mode, and shut down the
+ * Bluetooth audio path.
+ *
+ * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
+ * If this function returns true, this intent will be broadcasted with
+ * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_DISCONNECTED}.
+ *
+ * @param device Bluetooth headset
+ * @return false if there is no headset connected, or voice recognition has not started,
+ * or voice recognition has ended on this headset, or on error, true otherwise
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean stopVoiceRecognition(BluetoothDevice device) {
+ if (DBG) log("stopVoiceRecognition()");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.stopVoiceRecognition(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Check if Bluetooth SCO audio is connected.
+ *
+ * @param device Bluetooth headset
+ * @return true if SCO is connected, false otherwise or on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isAudioConnected(BluetoothDevice device) {
+ if (VDBG) log("isAudioConnected()");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.isAudioConnected(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothHeadset.STATE_AUDIO_DISCONNECTED,
+ BluetoothHeadset.STATE_AUDIO_CONNECTING,
+ BluetoothHeadset.STATE_AUDIO_CONNECTED,
+ BluetoothStatusCodes.ERROR_TIMEOUT
+ })
+ public @interface GetAudioStateReturnValues {}
+
+ /**
+ * Get the current audio state of the Headset.
+ *
+ * @param device is the Bluetooth device for which the audio state is being queried
+ * @return the audio state of the device or an error code
+ * @throws NullPointerException if the device is null
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @GetAudioStateReturnValues int getAudioState(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getAudioState");
+ Objects.requireNonNull(device);
+ final IBluetoothHeadset service = getService();
+ final int defaultValue = BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (!isDisabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getAudioState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ }
+ }
+ return defaultValue;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ BluetoothStatusCodes.ERROR_TIMEOUT,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ })
+ public @interface SetAudioRouteAllowedReturnValues {}
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.ALLOWED,
+ BluetoothStatusCodes.NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ BluetoothStatusCodes.ERROR_TIMEOUT,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ })
+ public @interface GetAudioRouteAllowedReturnValues {}
+
+ /**
+ * Sets whether audio routing is allowed. When set to {@code false}, the AG
+ * will not route any audio to the HF unless explicitly told to. This method
+ * should be used in cases where the SCO channel is shared between multiple
+ * profiles and must be delegated by a source knowledgeable.
+ *
+ * @param allowed {@code true} if the profile can reroute audio,
+ * {@code false} otherwise.
+ * @return {@link BluetoothStatusCodes#SUCCESS} upon successful setting,
+ * otherwise an error code.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @SetAudioRouteAllowedReturnValues int setAudioRouteAllowed(boolean allowed) {
+ if (VDBG) log("setAudioRouteAllowed");
+ final IBluetoothHeadset service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ service.setAudioRouteAllowed(allowed, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ return BluetoothStatusCodes.SUCCESS;
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ Log.e(TAG, "setAudioRouteAllowed: Bluetooth disabled, but profile service still bound");
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * @return {@link BluetoothStatusCodes#ALLOWED} if audio routing is allowed,
+ * {@link BluetoothStatusCodes#NOT_ALLOWED} if audio routing is not allowed, or
+ * an error code if an error occurs.
+ * see {@link #setAudioRouteAllowed(boolean)}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @GetAudioRouteAllowedReturnValues int getAudioRouteAllowed() {
+ if (VDBG) log("getAudioRouteAllowed");
+ final IBluetoothHeadset service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.getAudioRouteAllowed(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(false)
+ ? BluetoothStatusCodes.ALLOWED : BluetoothStatusCodes.NOT_ALLOWED;
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ Log.e(TAG, "getAudioRouteAllowed: Bluetooth disabled, but profile service still bound");
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Force SCO audio to be opened regardless any other restrictions
+ *
+ * @param forced Whether or not SCO audio connection should be forced: True to force SCO audio
+ * False to use SCO audio in normal manner
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setForceScoAudio(boolean forced) {
+ if (VDBG) log("setForceScoAudio " + String.valueOf(forced));
+ final IBluetoothHeadset service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ service.setForceScoAudio(forced, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ BluetoothStatusCodes.ERROR_TIMEOUT,
+ BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED,
+ BluetoothStatusCodes.ERROR_NO_ACTIVE_DEVICES,
+ BluetoothStatusCodes.ERROR_NOT_ACTIVE_DEVICE,
+ BluetoothStatusCodes.ERROR_AUDIO_ROUTE_BLOCKED,
+ BluetoothStatusCodes.ERROR_CALL_ACTIVE,
+ BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED
+ })
+ public @interface ConnectAudioReturnValues {}
+
+ /**
+ * Initiates a connection of SCO audio to the current active HFP device. The active HFP device
+ * can be identified with {@link BluetoothAdapter#getActiveDevices(int)}.
+ * <p>
+ * If this function returns {@link BluetoothStatusCodes#SUCCESS}, the intent
+ * {@link #ACTION_AUDIO_STATE_CHANGED} will be broadcasted twice. First with
+ * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}. This will be followed by a
+ * broadcast with {@link #EXTRA_STATE} set to either {@link #STATE_AUDIO_CONNECTED} if the audio
+ * connection is established or {@link #STATE_AUDIO_DISCONNECTED} if there was a failure in
+ * establishing the audio connection.
+ *
+ * @return whether the connection was successfully initiated or an error code on failure
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectAudioReturnValues int connectAudio() {
+ if (VDBG) log("connectAudio()");
+ final IBluetoothHeadset service = getService();
+ final int defaultValue = BluetoothStatusCodes.ERROR_UNKNOWN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.connectAudio(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ }
+ }
+
+ Log.e(TAG, "connectAudio: Bluetooth disabled, but profile service still bound");
+ return defaultValue;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ BluetoothStatusCodes.ERROR_TIMEOUT,
+ BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED,
+ BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_DISCONNECTED
+ })
+ public @interface DisconnectAudioReturnValues {}
+
+ /**
+ * Initiates a disconnection of HFP SCO audio from actively connected devices. It also tears
+ * down voice recognition or virtual voice call, if any exists.
+ *
+ * <p> If this function returns {@link BluetoothStatusCodes#SUCCESS}, the intent
+ * {@link #ACTION_AUDIO_STATE_CHANGED} will be broadcasted with {@link #EXTRA_STATE} set to
+ * {@link #STATE_AUDIO_DISCONNECTED}.
+ *
+ * @return whether the disconnection was initiated successfully or an error code on failure
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @DisconnectAudioReturnValues int disconnectAudio() {
+ if (VDBG) log("disconnectAudio()");
+ final IBluetoothHeadset service = getService();
+ final int defaultValue = BluetoothStatusCodes.ERROR_UNKNOWN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.disconnectAudio(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ return BluetoothStatusCodes.ERROR_TIMEOUT;
+ }
+ }
+
+ Log.e(TAG, "disconnectAudio: Bluetooth disabled, but profile service still bound");
+ return defaultValue;
+ }
+
+ /**
+ * Initiates a SCO channel connection as a virtual voice call to the current active device
+ * Active handsfree device will be notified of incoming call and connected call.
+ *
+ * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
+ * If this function returns true, this intent will be broadcasted with
+ * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}.
+ *
+ * <p> {@link #EXTRA_STATE} will transition from
+ * {@link #STATE_AUDIO_CONNECTING} to {@link #STATE_AUDIO_CONNECTED} when
+ * audio connection is established and to {@link #STATE_AUDIO_DISCONNECTED}
+ * in case of failure to establish the audio connection.
+ *
+ * @return true if successful, false if one of the following case applies
+ * - SCO audio is not idle (connecting or connected)
+ * - virtual call has already started
+ * - there is no active device
+ * - a Telecom managed call is going on
+ * - binder is dead or Bluetooth is disabled or other error
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean startScoUsingVirtualVoiceCall() {
+ if (DBG) log("startScoUsingVirtualVoiceCall()");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.startScoUsingVirtualVoiceCall(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Terminates an ongoing SCO connection and the associated virtual call.
+ *
+ * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
+ * If this function returns true, this intent will be broadcasted with
+ * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_DISCONNECTED}.
+ *
+ * @return true if successful, false if one of the following case applies
+ * - virtual voice call is not started or has ended
+ * - binder is dead or Bluetooth is disabled or other error
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean stopScoUsingVirtualVoiceCall() {
+ if (DBG) log("stopScoUsingVirtualVoiceCall()");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.stopScoUsingVirtualVoiceCall(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Notify Headset of phone state change.
+ * This is a backdoor for phone app to call BluetoothHeadset since
+ * there is currently not a good way to get precise call state change outside
+ * of phone app.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public void phoneStateChanged(int numActive, int numHeld, int callState, String number,
+ int type, String name) {
+ final IBluetoothHeadset service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.phoneStateChanged(numActive, numHeld, callState, number, type, name,
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Send Headset of CLCC response
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public void clccResponse(int index, int direction, int status, int mode, boolean mpty,
+ String number, int type) {
+ final IBluetoothHeadset service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ service.clccResponse(index, direction, status, mode, mpty, number, type,
+ mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Sends a vendor-specific unsolicited result code to the headset.
+ *
+ * <p>The actual string to be sent is <code>command + ": " + arg</code>. For example, if {@code
+ * command} is {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} and {@code arg} is {@code "0"}, the
+ * string <code>"+ANDROID: 0"</code> will be sent.
+ *
+ * <p>Currently only {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} is allowed as {@code command}.
+ *
+ * @param device Bluetooth headset.
+ * @param command A vendor-specific command.
+ * @param arg The argument that will be attached to the command.
+ * @return {@code false} if there is no headset connected, or if the command is not an allowed
+ * vendor-specific unsolicited result code, or on error. {@code true} otherwise.
+ * @throws IllegalArgumentException if {@code command} is {@code null}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendVendorSpecificResultCode(BluetoothDevice device, String command,
+ String arg) {
+ if (DBG) {
+ log("sendVendorSpecificResultCode()");
+ }
+ if (command == null) {
+ throw new IllegalArgumentException("command is null");
+ }
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.sendVendorSpecificResultCode(device, command, arg,
+ mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, in HFP and HSP profiles,
+ * it is the device used for phone call audio. If a remote device is not
+ * connected, it cannot be selected as active.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ * @param device Remote Bluetooth Device, could be null if phone call audio should not be
+ * streamed to a headset
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ @UnsupportedAppUsage(trackingBug = 171933273)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) {
+ Log.d(TAG, "setActiveDevice: " + device);
+ }
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && (device == null || isValidDevice(device))) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setActiveDevice(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the connected device that is active.
+ *
+ * @return the connected device that is active or null if no device
+ * is active.
+ * @hide
+ */
+ @UnsupportedAppUsage(trackingBug = 171933273)
+ @Nullable
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothDevice getActiveDevice() {
+ if (VDBG) Log.d(TAG, "getActiveDevice");
+ final IBluetoothHeadset service = getService();
+ final BluetoothDevice defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<BluetoothDevice> recv =
+ SynchronousResultReceiver.get();
+ service.getActiveDevice(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an
+ * active connection.
+ *
+ * @return true if in-band ringing is enabled, false if in-band ringing is disabled
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean isInbandRingingEnabled() {
+ if (DBG) log("isInbandRingingEnabled()");
+ final IBluetoothHeadset service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.isInbandRingingEnabled(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ @UnsupportedAppUsage
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private boolean isDisabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_OFF;
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothHeadsetClient.java b/android-34/android/bluetooth/BluetoothHeadsetClient.java
new file mode 100644
index 0000000..2f11cbf
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHeadsetClient.java
@@ -0,0 +1,1962 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the System APIs to interact with the Hands-Free Client profile.
+ *
+ * <p>BluetoothHeadsetClient is a proxy object for controlling the Bluetooth HFP Client
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothHeadsetClient proxy object.
+ *
+ * @hide
+ */
+@SystemApi
+public final class BluetoothHeadsetClient implements BluetoothProfile, AutoCloseable {
+ private static final String TAG = "BluetoothHeadsetClient";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+ private final CloseGuard mCloseGuard;
+
+ /**
+ * Intent used to broadcast the change in connection state of the HFP Client profile.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ *
+ * @hide
+ */
+ @SuppressLint("ActionValue")
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.headsetclient.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent sent whenever audio state changes.
+ *
+ * <p>It includes two mandatory extras:
+ * {@link BluetoothProfile#EXTRA_STATE},
+ * {@link BluetoothProfile#EXTRA_PREVIOUS_STATE},
+ * with possible values:
+ * {@link #STATE_AUDIO_CONNECTING},
+ * {@link #STATE_AUDIO_CONNECTED},
+ * {@link #STATE_AUDIO_DISCONNECTED}</p>
+ * <p>When <code>EXTRA_STATE</code> is set
+ * to </code>STATE_AUDIO_CONNECTED</code>,
+ * it also includes {@link #EXTRA_AUDIO_WBS}
+ * indicating wide band speech support.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_AUDIO_STATE_CHANGED =
+ "android.bluetooth.headsetclient.profile.action.AUDIO_STATE_CHANGED";
+
+ /**
+ * Intent sending updates of the Audio Gateway state.
+ * Each extra is being sent only when value it
+ * represents has been changed recently on AG.
+ * <p>It can contain one or more of the following extras:
+ * {@link #EXTRA_NETWORK_STATUS},
+ * {@link #EXTRA_NETWORK_SIGNAL_STRENGTH},
+ * {@link #EXTRA_NETWORK_ROAMING},
+ * {@link #EXTRA_BATTERY_LEVEL},
+ * {@link #EXTRA_OPERATOR_NAME},
+ * {@link #EXTRA_VOICE_RECOGNITION},
+ * {@link #EXTRA_IN_BAND_RING}</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AG_EVENT =
+ "android.bluetooth.headsetclient.profile.action.AG_EVENT";
+
+ /**
+ * Intent sent whenever state of a call changes.
+ *
+ * <p>It includes:
+ * {@link #EXTRA_CALL},
+ * with value of {@link BluetoothHeadsetClientCall} instance,
+ * representing actual call state.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CALL_CHANGED =
+ "android.bluetooth.headsetclient.profile.action.AG_CALL_CHANGED";
+
+ /**
+ * Intent that notifies about the result of the last issued action.
+ * Please note that not every action results in explicit action result code being sent.
+ * Instead other notifications about new Audio Gateway state might be sent,
+ * like <code>ACTION_AG_EVENT</code> with <code>EXTRA_VOICE_RECOGNITION</code> value
+ * when for example user started voice recognition from HF unit.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_RESULT =
+ "android.bluetooth.headsetclient.profile.action.RESULT";
+
+ /**
+ * Intent that notifies about vendor specific event arrival. Events not defined in
+ * HFP spec will be matched with supported vendor event list and this intent will
+ * be broadcasted upon a match. Supported vendor events are of format of
+ * of "+eventCode" or "+eventCode=xxxx" or "+eventCode:=xxxx".
+ * Vendor event can be a response to an vendor specific command or unsolicited.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_VENDOR_SPECIFIC_HEADSETCLIENT_EVENT =
+ "android.bluetooth.headsetclient.profile.action.VENDOR_SPECIFIC_EVENT";
+
+ /**
+ * Intent that notifies about the number attached to the last voice tag
+ * recorded on AG.
+ *
+ * <p>It contains:
+ * {@link #EXTRA_NUMBER},
+ * with a <code>String</code> value representing phone number.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_LAST_VTAG =
+ "android.bluetooth.headsetclient.profile.action.LAST_VTAG";
+
+ /**
+ * @hide
+ */
+ public static final int STATE_AUDIO_DISCONNECTED = 0;
+
+ /**
+ * @hide
+ */
+ public static final int STATE_AUDIO_CONNECTING = 1;
+
+ /**
+ * @hide
+ */
+ public static final int STATE_AUDIO_CONNECTED = 2;
+
+ /**
+ * Extra with information if connected audio is WBS.
+ * <p>Possible values: <code>true</code>,
+ * <code>false</code>.</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_AUDIO_WBS =
+ "android.bluetooth.headsetclient.extra.AUDIO_WBS";
+
+ /**
+ * Extra for AG_EVENT indicates network status.
+ * <p>Value: 0 - network unavailable,
+ * 1 - network available </p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_NETWORK_STATUS =
+ "android.bluetooth.headsetclient.extra.NETWORK_STATUS";
+
+ /**
+ * Extra for AG_EVENT intent indicates network signal strength.
+ * <p>Value: <code>Integer</code> representing signal strength.</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_NETWORK_SIGNAL_STRENGTH =
+ "android.bluetooth.headsetclient.extra.NETWORK_SIGNAL_STRENGTH";
+
+ /**
+ * Extra for AG_EVENT intent indicates roaming state.
+ * <p>Value: 0 - no roaming
+ * 1 - active roaming</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_NETWORK_ROAMING =
+ "android.bluetooth.headsetclient.extra.NETWORK_ROAMING";
+
+ /**
+ * Extra for AG_EVENT intent indicates the battery level.
+ * <p>Value: <code>Integer</code> representing signal strength.</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_BATTERY_LEVEL =
+ "android.bluetooth.headsetclient.extra.BATTERY_LEVEL";
+
+ /**
+ * Extra for AG_EVENT intent indicates operator name.
+ * <p>Value: <code>String</code> representing operator name.</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_OPERATOR_NAME =
+ "android.bluetooth.headsetclient.extra.OPERATOR_NAME";
+
+ /**
+ * Extra for AG_EVENT intent indicates voice recognition state.
+ * <p>Value:
+ * 0 - voice recognition stopped,
+ * 1 - voice recognition started.</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_VOICE_RECOGNITION =
+ "android.bluetooth.headsetclient.extra.VOICE_RECOGNITION";
+
+ /**
+ * Extra for AG_EVENT intent indicates in band ring state.
+ * <p>Value:
+ * 0 - in band ring tone not supported, or
+ * 1 - in band ring tone supported.</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_IN_BAND_RING =
+ "android.bluetooth.headsetclient.extra.IN_BAND_RING";
+
+ /**
+ * Extra for AG_EVENT intent indicates subscriber info.
+ * <p>Value: <code>String</code> containing subscriber information.</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_SUBSCRIBER_INFO =
+ "android.bluetooth.headsetclient.extra.SUBSCRIBER_INFO";
+
+ /**
+ * Extra for AG_CALL_CHANGED intent indicates the
+ * {@link BluetoothHeadsetClientCall} object that has changed.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CALL =
+ "android.bluetooth.headsetclient.extra.CALL";
+
+ /**
+ * Extra for ACTION_LAST_VTAG intent.
+ * <p>Value: <code>String</code> representing phone number
+ * corresponding to last voice tag recorded on AG</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_NUMBER =
+ "android.bluetooth.headsetclient.extra.NUMBER";
+
+ /**
+ * Extra for ACTION_RESULT intent that shows the result code of
+ * last issued action.
+ * <p>Possible results:
+ * {@link #ACTION_RESULT_OK},
+ * {@link #ACTION_RESULT_ERROR},
+ * {@link #ACTION_RESULT_ERROR_NO_CARRIER},
+ * {@link #ACTION_RESULT_ERROR_BUSY},
+ * {@link #ACTION_RESULT_ERROR_NO_ANSWER},
+ * {@link #ACTION_RESULT_ERROR_DELAYED},
+ * {@link #ACTION_RESULT_ERROR_BLACKLISTED},
+ * {@link #ACTION_RESULT_ERROR_CME}</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_RESULT_CODE =
+ "android.bluetooth.headsetclient.extra.RESULT_CODE";
+
+ /**
+ * Extra for ACTION_RESULT intent that shows the extended result code of
+ * last issued action.
+ * <p>Value: <code>Integer</code> - error code.</p>
+ *
+ * @hide
+ */
+ public static final String EXTRA_CME_CODE =
+ "android.bluetooth.headsetclient.extra.CME_CODE";
+
+ /**
+ * Extra for VENDOR_SPECIFIC_HEADSETCLIENT_EVENT intent that
+ * indicates vendor ID.
+ *
+ * @hide
+ */
+ public static final String EXTRA_VENDOR_ID =
+ "android.bluetooth.headsetclient.extra.VENDOR_ID";
+
+ /**
+ * Extra for VENDOR_SPECIFIC_HEADSETCLIENT_EVENT intent that
+ * indicates vendor event code.
+ *
+ * @hide
+ */
+ public static final String EXTRA_VENDOR_EVENT_CODE =
+ "android.bluetooth.headsetclient.extra.VENDOR_EVENT_CODE";
+
+ /**
+ * Extra for VENDOR_SPECIFIC_HEADSETCLIENT_EVENT intent that
+ * contains full vendor event including event code and full arguments.
+ *
+ * @hide
+ */
+ public static final String EXTRA_VENDOR_EVENT_FULL_ARGS =
+ "android.bluetooth.headsetclient.extra.VENDOR_EVENT_FULL_ARGS";
+
+ /* Extras for AG_FEATURES, extras type is boolean */
+ // TODO verify if all of those are actually useful
+ /**
+ * AG feature: three way calling.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_3WAY_CALLING =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_3WAY_CALLING";
+
+ /**
+ * AG feature: voice recognition.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_VOICE_RECOGNITION =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_VOICE_RECOGNITION";
+
+ /**
+ * AG feature: fetching phone number for voice tagging procedure.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT";
+
+ /**
+ * AG feature: ability to reject incoming call.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_REJECT_CALL =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_REJECT_CALL";
+
+ /**
+ * AG feature: enhanced call handling (terminate specific call, private consultation).
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_ECC =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_ECC";
+
+ /**
+ * AG feature: response and hold.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_RESPONSE_AND_HOLD =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_RESPONSE_AND_HOLD";
+
+ /**
+ * AG call handling feature: accept held or waiting call in three way calling scenarios.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_ACCEPT_HELD_OR_WAITING_CALL =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_ACCEPT_HELD_OR_WAITING_CALL";
+
+ /**
+ * AG call handling feature: release held or waiting call in three way calling scenarios.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_RELEASE_HELD_OR_WAITING_CALL =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_RELEASE_HELD_OR_WAITING_CALL";
+
+ /**
+ * AG call handling feature: release active call and accept held or waiting call in three way
+ * calling scenarios.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_RELEASE_AND_ACCEPT =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_RELEASE_AND_ACCEPT";
+
+ /**
+ * AG call handling feature: merge two calls, held and active - multi party conference mode.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_MERGE =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_MERGE";
+
+ /**
+ * AG call handling feature: merge calls and disconnect from multi party
+ * conversation leaving peers connected to each other.
+ * Note that this feature needs to be supported by mobile network operator
+ * as it requires connection and billing transfer.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AG_FEATURE_MERGE_AND_DETACH =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_MERGE_AND_DETACH";
+
+ /* Action result codes */
+ /**
+ * @hide
+ */
+ public static final int ACTION_RESULT_OK = 0;
+
+ /**
+ * @hide
+ */
+ public static final int ACTION_RESULT_ERROR = 1;
+
+ /**
+ * @hide
+ */
+ public static final int ACTION_RESULT_ERROR_NO_CARRIER = 2;
+
+ /**
+ * @hide
+ */
+ public static final int ACTION_RESULT_ERROR_BUSY = 3;
+
+ /**
+ * @hide
+ */
+ public static final int ACTION_RESULT_ERROR_NO_ANSWER = 4;
+
+ /**
+ * @hide
+ */
+ public static final int ACTION_RESULT_ERROR_DELAYED = 5;
+
+ /**
+ * @hide
+ */
+ public static final int ACTION_RESULT_ERROR_BLACKLISTED = 6;
+
+ /**
+ * @hide
+ */
+ public static final int ACTION_RESULT_ERROR_CME = 7;
+
+ /* Detailed CME error codes */
+ /**
+ * @hide
+ */
+ public static final int CME_PHONE_FAILURE = 0;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NO_CONNECTION_TO_PHONE = 1;
+
+ /**
+ * @hide
+ */
+ public static final int CME_OPERATION_NOT_ALLOWED = 3;
+
+ /**
+ * @hide
+ */
+ public static final int CME_OPERATION_NOT_SUPPORTED = 4;
+
+ /**
+ * @hide
+ */
+ public static final int CME_PHSIM_PIN_REQUIRED = 5;
+
+ /**
+ * @hide
+ */
+ public static final int CME_PHFSIM_PIN_REQUIRED = 6;
+
+ /**
+ * @hide
+ */
+ public static final int CME_PHFSIM_PUK_REQUIRED = 7;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SIM_NOT_INSERTED = 10;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SIM_PIN_REQUIRED = 11;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SIM_PUK_REQUIRED = 12;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SIM_FAILURE = 13;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SIM_BUSY = 14;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SIM_WRONG = 15;
+
+ /**
+ * @hide
+ */
+ public static final int CME_INCORRECT_PASSWORD = 16;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SIM_PIN2_REQUIRED = 17;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SIM_PUK2_REQUIRED = 18;
+
+ /**
+ * @hide
+ */
+ public static final int CME_MEMORY_FULL = 20;
+
+ /**
+ * @hide
+ */
+ public static final int CME_INVALID_INDEX = 21;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NOT_FOUND = 22;
+
+ /**
+ * @hide
+ */
+ public static final int CME_MEMORY_FAILURE = 23;
+
+ /**
+ * @hide
+ */
+ public static final int CME_TEXT_STRING_TOO_LONG = 24;
+
+ /**
+ * @hide
+ */
+ public static final int CME_INVALID_CHARACTER_IN_TEXT_STRING = 25;
+
+ /**
+ * @hide
+ */
+ public static final int CME_DIAL_STRING_TOO_LONG = 26;
+
+ /**
+ * @hide
+ */
+ public static final int CME_INVALID_CHARACTER_IN_DIAL_STRING = 27;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NO_NETWORK_SERVICE = 30;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NETWORK_TIMEOUT = 31;
+
+ /**
+ * @hide
+ */
+ public static final int CME_EMERGENCY_SERVICE_ONLY = 32;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NO_SIMULTANOUS_VOIP_CS_CALLS = 33;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NOT_SUPPORTED_FOR_VOIP = 34;
+ /**
+ * @hide
+ */
+ public static final int CME_SIP_RESPONSE_CODE = 35;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NETWORK_PERSONALIZATION_PIN_REQUIRED = 40;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NETWORK_PERSONALIZATION_PUK_REQUIRED = 41;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NETWORK_SUBSET_PERSONALIZATION_PIN_REQUIRED = 42;
+
+ /**
+ * @hide
+ */
+ public static final int CME_NETWORK_SUBSET_PERSONALIZATION_PUK_REQUIRED = 43;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SERVICE_PROVIDER_PERSONALIZATION_PIN_REQUIRED = 44;
+
+ /**
+ * @hide
+ */
+ public static final int CME_SERVICE_PROVIDER_PERSONALIZATION_PUK_REQUIRED = 45;
+
+ /**
+ * @hide
+ */
+ public static final int CME_CORPORATE_PERSONALIZATION_PIN_REQUIRED = 46;
+
+ /**
+ * @hide
+ */
+ public static final int CME_CORPORATE_PERSONALIZATION_PUK_REQUIRED = 47;
+
+ /**
+ * @hide
+ */
+ public static final int CME_HIDDEN_KEY_REQUIRED = 48;
+
+ /**
+ * @hide
+ */
+ public static final int CME_EAP_NOT_SUPPORTED = 49;
+
+ /**
+ * @hide
+ */
+ public static final int CME_INCORRECT_PARAMETERS = 50;
+
+ /* Action policy for other calls when accepting call */
+ /**
+ * @hide
+ */
+ public static final int CALL_ACCEPT_NONE = 0;
+
+ /**
+ * @hide
+ */
+ public static final int CALL_ACCEPT_HOLD = 1;
+
+ /**
+ * @hide
+ */
+ public static final int CALL_ACCEPT_TERMINATE = 2;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHeadsetClient> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HEADSET_CLIENT,
+ "BluetoothHeadsetClient", IBluetoothHeadsetClient.class.getName()) {
+ @Override
+ public IBluetoothHeadsetClient getServiceInterface(IBinder service) {
+ return IBluetoothHeadsetClient.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothHeadsetClient proxy object.
+ */
+ BluetoothHeadsetClient(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Close the connection to the backing service. Other public functions of BluetoothHeadsetClient
+ * will return default error results once close() has been called. Multiple invocations of
+ * close() are ok.
+ *
+ * @hide
+ */
+ @Override
+ public void close() {
+ if (VDBG) log("close()");
+ mProfileConnector.disconnect();
+ if (mCloseGuard != null) {
+ mCloseGuard.close();
+ }
+ }
+
+ private IBluetoothHeadsetClient getService() {
+ return mProfileConnector.getService();
+ }
+
+ /** @hide */
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /**
+ * Connects to remote device.
+ *
+ * Currently, the system supports only 1 connection. So, in case of the
+ * second connection, this implementation will disconnect already connected
+ * device automatically and will process the new one.
+ *
+ * @param device a remote device we want connect to
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CONNECTION_STATE_CHANGED} intent.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.connect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Disconnects remote device
+ *
+ * @param device a remote device we want disconnect
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CONNECTION_STATE_CHANGED} intent.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.disconnect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
+ @SystemApi
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothHeadsetClient service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
+ @SystemApi
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @NonNull
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothHeadsetClient service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
+ @SystemApi
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @BtProfileState int getConnectionState(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getConnectionState(" + device + ")");
+ final IBluetoothHeadsetClient service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set priority of the profile
+ *
+ * <p> The device should already be paired.
+ * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF}
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ if (DBG) log("setPriority(" + device + ", " + priority + ")");
+ return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority));
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true if connectionPolicy is set, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)
+ && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the priority of the profile.
+ *
+ * <p> The priority can be any of:
+ * {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getPriority(BluetoothDevice device) {
+ if (VDBG) log("getPriority(" + device + ")");
+ return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothHeadsetClient service = getService();
+ final @ConnectionPolicy int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionPolicy(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Starts voice recognition.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_AG_EVENT} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_VOICE_RECOGNITION}. This method invocation will fail silently when feature
+ * is not supported.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean startVoiceRecognition(BluetoothDevice device) {
+ if (DBG) log("startVoiceRecognition()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.startVoiceRecognition(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Send vendor specific AT command.
+ *
+ * @param device remote device
+ * @param vendorId vendor number by Bluetooth SIG
+ * @param atCommand command to be sent. It start with + prefix and only one command at one time.
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendVendorAtCommand(BluetoothDevice device, int vendorId, String atCommand) {
+ if (DBG) log("sendVendorSpecificCommand()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.sendVendorAtCommand(device, vendorId, atCommand, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Stops voice recognition.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_AG_EVENT} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_VOICE_RECOGNITION}. This method invocation will fail silently when feature
+ * is not supported.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean stopVoiceRecognition(BluetoothDevice device) {
+ if (DBG) log("stopVoiceRecognition()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.stopVoiceRecognition(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns list of all calls in any state.
+ *
+ * @param device remote device
+ * @return list of calls; empty list if none call exists
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothHeadsetClientCall> getCurrentCalls(BluetoothDevice device) {
+ if (DBG) log("getCurrentCalls()");
+ final IBluetoothHeadsetClient service = getService();
+ final List<BluetoothHeadsetClientCall> defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothHeadsetClientCall>> recv =
+ SynchronousResultReceiver.get();
+ service.getCurrentCalls(device, mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns list of current values of AG indicators.
+ *
+ * @param device remote device
+ * @return bundle of AG indicators; null if device is not in CONNECTED state
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public Bundle getCurrentAgEvents(BluetoothDevice device) {
+ if (DBG) log("getCurrentAgEvents()");
+ final IBluetoothHeadsetClient service = getService();
+ final Bundle defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Bundle> recv = SynchronousResultReceiver.get();
+ service.getCurrentAgEvents(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Accepts a call
+ *
+ * @param device remote device
+ * @param flag action policy while accepting a call. Possible values {@link #CALL_ACCEPT_NONE},
+ * {@link #CALL_ACCEPT_HOLD}, {@link #CALL_ACCEPT_TERMINATE}
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean acceptCall(BluetoothDevice device, int flag) {
+ if (DBG) log("acceptCall()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.acceptCall(device, flag, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Holds a call.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean holdCall(BluetoothDevice device) {
+ if (DBG) log("holdCall()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.holdCall(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Rejects a call.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_REJECT_CALL}. This method invocation will fail silently when feature is not
+ * supported.</p>
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean rejectCall(BluetoothDevice device) {
+ if (DBG) log("rejectCall()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.rejectCall(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Terminates a specified call.
+ *
+ * Works only when Extended Call Control is supported by Audio Gateway.
+ *
+ * @param device remote device
+ * @param call Handle of call obtained in {@link #dial(BluetoothDevice, String)} or obtained via
+ * {@link #ACTION_CALL_CHANGED}. {@code call} may be null in which case we will hangup all active
+ * calls.
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_ECC}. This method invocation will fail silently when feature is not
+ * supported.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean terminateCall(BluetoothDevice device, BluetoothHeadsetClientCall call) {
+ if (DBG) log("terminateCall()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.terminateCall(device, call, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Enters private mode with a specified call.
+ *
+ * Works only when Extended Call Control is supported by Audio Gateway.
+ *
+ * @param device remote device
+ * @param index index of the call to connect in private mode
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_ECC}. This method invocation will fail silently when feature is not
+ * supported.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean enterPrivateMode(BluetoothDevice device, int index) {
+ if (DBG) log("enterPrivateMode()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.enterPrivateMode(device, index, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Performs explicit call transfer.
+ *
+ * That means connect other calls and disconnect.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_MERGE_AND_DETACH}. This method invocation will fail silently when feature
+ * is not supported.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean explicitCallTransfer(BluetoothDevice device) {
+ if (DBG) log("explicitCallTransfer()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.explicitCallTransfer(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Places a call with specified number.
+ *
+ * @param device remote device
+ * @param number valid phone number
+ * @return <code>{@link BluetoothHeadsetClientCall} call</code> if command has been issued
+ * successfully; <code>{@link null}</code> otherwise; upon completion HFP sends {@link
+ * #ACTION_CALL_CHANGED} intent in case of success; {@link #ACTION_RESULT} is sent otherwise;
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothHeadsetClientCall dial(BluetoothDevice device, String number) {
+ if (DBG) log("dial()");
+ final IBluetoothHeadsetClient service = getService();
+ final BluetoothHeadsetClientCall defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<BluetoothHeadsetClientCall> recv =
+ SynchronousResultReceiver.get();
+ service.dial(device, number, mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sends DTMF code.
+ *
+ * Possible code values : 0,1,2,3,4,5,6,7,8,9,A,B,C,D,*,#
+ *
+ * @param device remote device
+ * @param code ASCII code
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_RESULT} intent;
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendDTMF(BluetoothDevice device, byte code) {
+ if (DBG) log("sendDTMF()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.sendDTMF(device, code, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get a number corresponding to last voice tag recorded on AG.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_LAST_VTAG} or {@link #ACTION_RESULT}
+ * intent;
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT}. This method invocation will fail silently when
+ * feature is not supported.</p>
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean getLastVoiceTagNumber(BluetoothDevice device) {
+ if (DBG) log("getLastVoiceTagNumber()");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.getLastVoiceTagNumber(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns current audio state of Audio Gateway.
+ *
+ * Note: This is an internal function and shouldn't be exposed
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getAudioState(BluetoothDevice device) {
+ if (VDBG) log("getAudioState");
+ final IBluetoothHeadsetClient service = getService();
+ final int defaultValue = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getAudioState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ } else {
+ return defaultValue;
+ }
+ return BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
+ }
+
+ /**
+ * Sets whether audio routing is allowed.
+ *
+ * @param device remote device
+ * @param allowed if routing is allowed to the device Note: This is an internal function and
+ * shouldn't be exposed
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setAudioRouteAllowed(BluetoothDevice device, boolean allowed) {
+ if (VDBG) log("setAudioRouteAllowed");
+ final IBluetoothHeadsetClient service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ service.setAudioRouteAllowed(device, allowed, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Returns whether audio routing is allowed.
+ *
+ * @param device remote device
+ * @return whether the command succeeded Note: This is an internal function and shouldn't be
+ * exposed
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean getAudioRouteAllowed(BluetoothDevice device) {
+ if (VDBG) log("getAudioRouteAllowed");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.getAudioRouteAllowed(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Initiates a connection of audio channel.
+ *
+ * It setup SCO channel with remote connected Handsfree AG device.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_AUDIO_STATE_CHANGED} intent;
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connectAudio(BluetoothDevice device) {
+ if (VDBG) log("connectAudio");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.connectAudio(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Disconnects audio channel.
+ *
+ * It tears down the SCO channel from remote AG device.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_AUDIO_STATE_CHANGED} intent;
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnectAudio(BluetoothDevice device) {
+ if (VDBG) log("disconnectAudio");
+ final IBluetoothHeadsetClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.disconnectAudio(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get Audio Gateway features
+ *
+ * @param device remote device
+ * @return bundle of AG features; null if no service or AG not connected
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public Bundle getCurrentAgFeatures(BluetoothDevice device) {
+ if (VDBG) log("getCurrentAgFeatures");
+ final IBluetoothHeadsetClient service = getService();
+ final Bundle defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Bundle> recv = SynchronousResultReceiver.get();
+ service.getCurrentAgFeatures(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * A class that contains the network service info provided by the HFP Client profile
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final class NetworkServiceState implements Parcelable {
+ /** The device associated with this service state */
+ private final BluetoothDevice mDevice;
+
+ /** True if there is service available, False otherwise */
+ private final boolean mIsServiceAvailable;
+
+ /** The name of the operator associated with the remote device's current network */
+ private final String mOperatorName;
+
+ /**
+ * The general signal strength, from 0 to 5
+ */
+ private final int mSignalStrength;
+
+ /** True if we are network roaming, False otherwise */
+ private final boolean mIsRoaming;
+
+ /**
+ * Create a NetworkServiceState Object
+ *
+ * @param device The device associated with this network signal state
+ * @param isServiceAvailable True if there is service available, False otherwise
+ * @param operatorName The name of the operator associated with the remote device's current
+ * network. Use Null if the value is unknown
+ * @param signalStrength The general signal strength
+ * @param isRoaming True if we are network roaming, False otherwise
+ *
+ * @hide
+ */
+ public NetworkServiceState(BluetoothDevice device, boolean isServiceAvailable,
+ String operatorName, int signalStrength, boolean isRoaming) {
+ mDevice = device;
+ mIsServiceAvailable = isServiceAvailable;
+ mOperatorName = operatorName;
+ mSignalStrength = signalStrength;
+ mIsRoaming = isRoaming;
+ }
+
+ /**
+ * Get the device associated with this network service state
+ *
+ * @return a BluetoothDevice associated with this state
+ *
+ * @hide
+ */
+ @SystemApi
+ public @NonNull BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Get the network service availablility state
+ *
+ * @return True if there is service available, False otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isServiceAvailable() {
+ return mIsServiceAvailable;
+ }
+
+ /**
+ * Get the network operator name
+ *
+ * @return A string representing the name of the operator the remote device is on, or null
+ * if unknown.
+ *
+ * @hide
+ */
+ @SystemApi
+ public @Nullable String getNetworkOperatorName() {
+ return mOperatorName;
+ }
+
+ /**
+ * The HFP Client defined signal strength, from 0 to 5.
+ *
+ * Bluetooth HFP v1.8 specifies that the signal strength of a device can be [0, 5]. It does
+ * not place any requirements on how a device derives those values. While they're typically
+ * derived from signal quality/RSSI buckets, there's no way to be certain on the exact
+ * meaning. Derivation methods can even change between wireless cellular technologies.
+ *
+ * That said, you can "generally" interpret the values relative to each other as follows:
+ * - Level 0: None/Unknown
+ * - Level 1: Very Poor
+ * - Level 2: Poor
+ * - Level 3: Fair
+ * - Level 4: Good
+ * - Level 5: Great
+ *
+ * @return the HFP Client defined signal strength, range [0, 5]
+ *
+ * @hide
+ */
+ @SystemApi
+ public @IntRange(from = 0, to = 5) int getSignalStrength() {
+ return mSignalStrength;
+ }
+
+ /**
+ * Get the network service roaming status
+ *
+ * * @return True if we are network roaming, False otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isRoaming() {
+ return mIsRoaming;
+ }
+
+ /**
+ * {@link Parcelable.Creator} interface implementation.
+ */
+ public static final @NonNull Parcelable.Creator<NetworkServiceState> CREATOR =
+ new Parcelable.Creator<NetworkServiceState>() {
+ public NetworkServiceState createFromParcel(Parcel in) {
+ return new NetworkServiceState((BluetoothDevice) in.readParcelable(null),
+ in.readInt() == 1, in.readString(), in.readInt(), in.readInt() == 1);
+ }
+
+ public @NonNull NetworkServiceState[] newArray(int size) {
+ return new NetworkServiceState[size];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeParcelable(mDevice, 0);
+ out.writeInt(mIsServiceAvailable ? 1 : 0);
+ out.writeString(mOperatorName);
+ out.writeInt(mSignalStrength);
+ out.writeInt(mIsRoaming ? 1 : 0);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+ }
+
+ /**
+ * Intent used to broadcast the change in network service state of an HFP Client device
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link EXTRA_NETWORK_SERVICE_STATE} - A {@link NetworkServiceState} object. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SuppressLint("ActionValue")
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NETWORK_SERVICE_STATE_CHANGED =
+ "android.bluetooth.headsetclient.profile.action.NETWORK_SERVICE_STATE_CHANGED";
+
+ /**
+ * Extra for the network service state changed intent.
+ *
+ * This extra represents the current network service state of a connected Bluetooth device.
+ *
+ * @hide
+ */
+ @SuppressLint("ActionValue")
+ @SystemApi
+ public static final String EXTRA_NETWORK_SERVICE_STATE =
+ "android.bluetooth.headsetclient.extra.EXTRA_NETWORK_SERVICE_STATE";
+
+ /**
+ * Get the network service state for a device
+ *
+ * @param device The {@link BluetoothDevice} you want the network service state for
+ * @return A {@link NetworkServiceState} representing the network service state of the device,
+ * or null if the device is not connected
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Nullable NetworkServiceState getNetworkServiceState(@NonNull BluetoothDevice device) {
+ if (device == null) {
+ return null;
+ }
+
+ Bundle agEvents = getCurrentAgEvents(device);
+ if (agEvents == null) {
+ return null;
+ }
+
+ boolean isServiceAvailable = (agEvents.getInt(EXTRA_NETWORK_STATUS, 0) == 1);
+ int signalStrength = agEvents.getInt(EXTRA_NETWORK_SIGNAL_STRENGTH, 0);
+ String operatorName = agEvents.getString(EXTRA_OPERATOR_NAME, null);
+ boolean isRoaming = (agEvents.getInt(EXTRA_NETWORK_ROAMING, 0) == 1);
+
+ return new NetworkServiceState(device, isServiceAvailable, operatorName, signalStrength,
+ isRoaming);
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothHeadsetClientCall.java b/android-34/android/bluetooth/BluetoothHeadsetClientCall.java
new file mode 100644
index 0000000..a33d8bd
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHeadsetClientCall.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+
+import java.util.UUID;
+
+/**
+ * This class represents a single call, its state and properties.
+ * It implements {@link Parcelable} for inter-process message passing.
+ *
+ * @hide
+ */
+public final class BluetoothHeadsetClientCall implements Parcelable, Attributable {
+
+ /* Call state */
+ /**
+ * Call is active.
+ */
+ public static final int CALL_STATE_ACTIVE = 0;
+ /**
+ * Call is in held state.
+ */
+ public static final int CALL_STATE_HELD = 1;
+ /**
+ * Outgoing call that is being dialed right now.
+ */
+ public static final int CALL_STATE_DIALING = 2;
+ /**
+ * Outgoing call that remote party has already been alerted about.
+ */
+ public static final int CALL_STATE_ALERTING = 3;
+ /**
+ * Incoming call that can be accepted or rejected.
+ */
+ public static final int CALL_STATE_INCOMING = 4;
+ /**
+ * Waiting call state when there is already an active call.
+ */
+ public static final int CALL_STATE_WAITING = 5;
+ /**
+ * Call that has been held by response and hold
+ * (see Bluetooth specification for further references).
+ */
+ public static final int CALL_STATE_HELD_BY_RESPONSE_AND_HOLD = 6;
+ /**
+ * Call that has been already terminated and should not be referenced as a valid call.
+ */
+ public static final int CALL_STATE_TERMINATED = 7;
+
+ private final BluetoothDevice mDevice;
+ private final int mId;
+ private int mState;
+ private String mNumber;
+ private boolean mMultiParty;
+ private final boolean mOutgoing;
+ private final UUID mUUID;
+ private final long mCreationElapsedMilli;
+ private final boolean mInBandRing;
+
+ /**
+ * Creates BluetoothHeadsetClientCall instance.
+ */
+ public BluetoothHeadsetClientCall(BluetoothDevice device, int id, int state, String number,
+ boolean multiParty, boolean outgoing, boolean inBandRing) {
+ this(device, id, UUID.randomUUID(), state, number, multiParty, outgoing, inBandRing);
+ }
+
+ public BluetoothHeadsetClientCall(BluetoothDevice device, int id, UUID uuid, int state,
+ String number, boolean multiParty, boolean outgoing, boolean inBandRing) {
+ mDevice = device;
+ mId = id;
+ mUUID = uuid;
+ mState = state;
+ mNumber = number != null ? number : "";
+ mMultiParty = multiParty;
+ mOutgoing = outgoing;
+ mInBandRing = inBandRing;
+ mCreationElapsedMilli = SystemClock.elapsedRealtime();
+ }
+
+ /** {@hide} */
+ public void setAttributionSource(@NonNull AttributionSource attributionSource) {
+ Attributable.setAttributionSource(mDevice, attributionSource);
+ }
+
+ /**
+ * Sets call's state.
+ *
+ * <p>Note: This is an internal function and shouldn't be exposed</p>
+ *
+ * @param state new call state.
+ */
+ public void setState(int state) {
+ mState = state;
+ }
+
+ /**
+ * Sets call's number.
+ *
+ * <p>Note: This is an internal function and shouldn't be exposed</p>
+ *
+ * @param number String representing phone number.
+ */
+ public void setNumber(String number) {
+ mNumber = number;
+ }
+
+ /**
+ * Sets this call as multi party call.
+ *
+ * <p>Note: This is an internal function and shouldn't be exposed</p>
+ *
+ * @param multiParty if <code>true</code> sets this call as a part of multi party conference.
+ */
+ public void setMultiParty(boolean multiParty) {
+ mMultiParty = multiParty;
+ }
+
+ /**
+ * Gets call's device.
+ *
+ * @return call device.
+ */
+ public BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Gets call's Id.
+ *
+ * @return call id.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Gets call's UUID.
+ *
+ * @return call uuid
+ * @hide
+ */
+ public UUID getUUID() {
+ return mUUID;
+ }
+
+ /**
+ * Gets call's current state.
+ *
+ * @return state of this particular phone call.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Gets call's number.
+ *
+ * @return string representing phone number.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public String getNumber() {
+ return mNumber;
+ }
+
+ /**
+ * Gets call's creation time in millis since epoch.
+ *
+ * @return long representing the creation time.
+ */
+ public long getCreationElapsedMilli() {
+ return mCreationElapsedMilli;
+ }
+
+ /**
+ * Checks if call is an active call in a conference mode (aka multi party).
+ *
+ * @return <code>true</code> if call is a multi party call, <code>false</code> otherwise.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean isMultiParty() {
+ return mMultiParty;
+ }
+
+ /**
+ * Checks if this call is an outgoing call.
+ *
+ * @return <code>true</code> if its outgoing call, <code>false</code> otherwise.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean isOutgoing() {
+ return mOutgoing;
+ }
+
+ /**
+ * Checks if the ringtone will be generated by the connected phone
+ *
+ * @return <code>true</code> if in band ring is enabled, <code>false</code> otherwise.
+ */
+ public boolean isInBandRing() {
+ return mInBandRing;
+ }
+
+
+ @Override
+ public String toString() {
+ return toString(false);
+ }
+
+ /**
+ * Generate a log string for this call
+ * @param loggable whether device address should be logged
+ * @return log string
+ */
+ public String toString(boolean loggable) {
+ StringBuilder builder = new StringBuilder("BluetoothHeadsetClientCall{mDevice: ");
+ builder.append(loggable ? mDevice : mDevice.hashCode());
+ builder.append(", mId: ");
+ builder.append(mId);
+ builder.append(", mUUID: ");
+ builder.append(mUUID);
+ builder.append(", mState: ");
+ switch (mState) {
+ case CALL_STATE_ACTIVE:
+ builder.append("ACTIVE");
+ break;
+ case CALL_STATE_HELD:
+ builder.append("HELD");
+ break;
+ case CALL_STATE_DIALING:
+ builder.append("DIALING");
+ break;
+ case CALL_STATE_ALERTING:
+ builder.append("ALERTING");
+ break;
+ case CALL_STATE_INCOMING:
+ builder.append("INCOMING");
+ break;
+ case CALL_STATE_WAITING:
+ builder.append("WAITING");
+ break;
+ case CALL_STATE_HELD_BY_RESPONSE_AND_HOLD:
+ builder.append("HELD_BY_RESPONSE_AND_HOLD");
+ break;
+ case CALL_STATE_TERMINATED:
+ builder.append("TERMINATED");
+ break;
+ default:
+ builder.append(mState);
+ break;
+ }
+ builder.append(", mNumber: ");
+ builder.append(loggable ? mNumber : mNumber.hashCode());
+ builder.append(", mMultiParty: ");
+ builder.append(mMultiParty);
+ builder.append(", mOutgoing: ");
+ builder.append(mOutgoing);
+ builder.append(", mInBandRing: ");
+ builder.append(mInBandRing);
+ builder.append("}");
+ return builder.toString();
+ }
+
+ /**
+ * {@link Parcelable.Creator} interface implementation.
+ */
+ public static final @NonNull Creator<BluetoothHeadsetClientCall> CREATOR = new Creator<>() {
+ @Override
+ public BluetoothHeadsetClientCall createFromParcel(Parcel in) {
+ return new BluetoothHeadsetClientCall((BluetoothDevice) in.readParcelable(null),
+ in.readInt(), UUID.fromString(in.readString()), in.readInt(),
+ in.readString(), in.readInt() == 1, in.readInt() == 1,
+ in.readInt() == 1);
+ }
+
+ @Override
+ public BluetoothHeadsetClientCall[] newArray(int size) {
+ return new BluetoothHeadsetClientCall[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(mDevice, 0);
+ out.writeInt(mId);
+ out.writeString(mUUID.toString());
+ out.writeInt(mState);
+ out.writeString(mNumber);
+ out.writeInt(mMultiParty ? 1 : 0);
+ out.writeInt(mOutgoing ? 1 : 0);
+ out.writeInt(mInBandRing ? 1 : 0);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothHealth.java b/android-34/android/bluetooth/BluetoothHealth.java
new file mode 100644
index 0000000..a155e4f
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHealth.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Public API for Bluetooth Health Profile.
+ *
+ * <p>BluetoothHealth is a proxy object for controlling the Bluetooth
+ * Service via IPC.
+ *
+ * <p> How to connect to a health device which is acting in the source role.
+ * <li> Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothHealth proxy object. </li>
+ * <li> Create an {@link BluetoothHealth} callback and call
+ * {@link #registerSinkAppConfiguration} to register an application
+ * configuration </li>
+ * <li> Pair with the remote device. This currently needs to be done manually
+ * from Bluetooth Settings </li>
+ * <li> Connect to a health device using {@link #connectChannelToSource}. Some
+ * devices will connect the channel automatically. The {@link BluetoothHealth}
+ * callback will inform the application of channel state change. </li>
+ * <li> Use the file descriptor provided with a connected channel to read and
+ * write data to the health channel. </li>
+ * <li> The received data needs to be interpreted using a health manager which
+ * implements the IEEE 11073-xxxxx specifications.
+ * <li> When done, close the health channel by calling {@link #disconnectChannel}
+ * and unregister the application configuration calling
+ * {@link #unregisterAppConfiguration}
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New apps
+ * should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+@Deprecated
+public final class BluetoothHealth implements BluetoothProfile {
+ private static final String TAG = "BluetoothHealth";
+ /**
+ * Health Profile Source Role - the health device.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int SOURCE_ROLE = 1 << 0;
+
+ /**
+ * Health Profile Sink Role the device talking to the health device.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int SINK_ROLE = 1 << 1;
+
+ /**
+ * Health Profile - Channel Type used - Reliable
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int CHANNEL_TYPE_RELIABLE = 10;
+
+ /**
+ * Health Profile - Channel Type used - Streaming
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int CHANNEL_TYPE_STREAMING = 11;
+
+ /**
+ * Hide auto-created default constructor
+ * @hide
+ */
+ BluetoothHealth() {}
+
+ /**
+ * Register an application configuration that acts as a Health SINK.
+ * This is the configuration that will be used to communicate with health devices
+ * which will act as the {@link #SOURCE_ROLE}. This is an asynchronous call and so
+ * the callback is used to notify success or failure if the function returns true.
+ *
+ * @param name The friendly name associated with the application or configuration.
+ * @param dataType The dataType of the Source role of Health Profile to which the sink wants to
+ * connect to.
+ * @param callback A callback to indicate success or failure of the registration and all
+ * operations done on this application configuration.
+ * @return If true, callback will be called.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean registerSinkAppConfiguration(String name, int dataType,
+ BluetoothHealthCallback callback) {
+ Log.e(TAG, "registerSinkAppConfiguration(): BluetoothHealth is deprecated");
+ return false;
+ }
+
+ /**
+ * Unregister an application configuration that has been registered using
+ * {@link #registerSinkAppConfiguration}
+ *
+ * @param config The health app configuration
+ * @return Success or failure.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) {
+ Log.e(TAG, "unregisterAppConfiguration(): BluetoothHealth is deprecated");
+ return false;
+ }
+
+ /**
+ * Connect to a health device which has the {@link #SOURCE_ROLE}.
+ * This is an asynchronous call. If this function returns true, the callback
+ * associated with the application configuration will be called.
+ *
+ * @param device The remote Bluetooth device.
+ * @param config The application configuration which has been registered using {@link
+ * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
+ * @return If true, the callback associated with the application config will be called.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean connectChannelToSource(BluetoothDevice device,
+ BluetoothHealthAppConfiguration config) {
+ Log.e(TAG, "connectChannelToSource(): BluetoothHealth is deprecated");
+ return false;
+ }
+
+ /**
+ * Disconnect a connected health channel.
+ * This is an asynchronous call. If this function returns true, the callback
+ * associated with the application configuration will be called.
+ *
+ * @param device The remote Bluetooth device.
+ * @param config The application configuration which has been registered using {@link
+ * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
+ * @param channelId The channel id associated with the channel
+ * @return If true, the callback associated with the application config will be called.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean disconnectChannel(BluetoothDevice device,
+ BluetoothHealthAppConfiguration config, int channelId) {
+ Log.e(TAG, "disconnectChannel(): BluetoothHealth is deprecated");
+ return false;
+ }
+
+ /**
+ * Get the file descriptor of the main channel associated with the remote device
+ * and application configuration.
+ *
+ * <p> Its the responsibility of the caller to close the ParcelFileDescriptor
+ * when done.
+ *
+ * @param device The remote Bluetooth health device
+ * @param config The application configuration
+ * @return null on failure, ParcelFileDescriptor on success.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device,
+ BluetoothHealthAppConfiguration config) {
+ Log.e(TAG, "getMainChannelFd(): BluetoothHealth is deprecated");
+ return null;
+ }
+
+ /**
+ * Get the current connection state of the profile.
+ *
+ * This is not specific to any application configuration but represents the connection
+ * state of the local Bluetooth adapter with the remote device. This can be used
+ * by applications like status bar which would just like to know the state of the
+ * local adapter.
+ *
+ * @param device Remote bluetooth device.
+ * @return State of the profile connection. One of {@link #STATE_CONNECTED}, {@link
+ * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}
+ */
+ @Override
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public int getConnectionState(BluetoothDevice device) {
+ Log.e(TAG, "getConnectionState(): BluetoothHealth is deprecated");
+ return STATE_DISCONNECTED;
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+
+ /**
+ * Get connected devices for the health profile.
+ *
+ * <p> Return the set of devices which are in state {@link #STATE_CONNECTED}
+ *
+ * This is not specific to any application configuration but represents the connection
+ * state of the local Bluetooth adapter for this profile. This can be used
+ * by applications like status bar which would just like to know the state of the
+ * local adapter.
+ *
+ * @return List of devices. The list will be empty on error.
+ */
+ @Override
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public List<BluetoothDevice> getConnectedDevices() {
+ Log.e(TAG, "getConnectedDevices(): BluetoothHealth is deprecated");
+ return new ArrayList<>();
+ }
+
+ /**
+ * Get a list of devices that match any of the given connection
+ * states.
+ *
+ * <p> If none of the devices match any of the given states,
+ * an empty list will be returned.
+ *
+ * <p>This is not specific to any application configuration but represents the connection
+ * state of the local Bluetooth adapter for this profile. This can be used
+ * by applications like status bar which would just like to know the state of the
+ * local adapter.
+ *
+ * @param states Array of states. States can be one of {@link #STATE_CONNECTED}, {@link
+ * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING},
+ * @return List of devices. The list will be empty on error.
+ */
+ @Override
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ Log.e(TAG, "getDevicesMatchingConnectionStates(): BluetoothHealth is deprecated");
+ return new ArrayList<>();
+ }
+
+ /** Health Channel Connection State - Disconnected
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int STATE_CHANNEL_DISCONNECTED = 0;
+ /** Health Channel Connection State - Connecting
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int STATE_CHANNEL_CONNECTING = 1;
+ /** Health Channel Connection State - Connected
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int STATE_CHANNEL_CONNECTED = 2;
+ /** Health Channel Connection State - Disconnecting
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int STATE_CHANNEL_DISCONNECTING = 3;
+
+ /** Health App Configuration registration success
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int APP_CONFIG_REGISTRATION_SUCCESS = 0;
+ /** Health App Configuration registration failure
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int APP_CONFIG_REGISTRATION_FAILURE = 1;
+ /** Health App Configuration un-registration success
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int APP_CONFIG_UNREGISTRATION_SUCCESS = 2;
+ /** Health App Configuration un-registration failure
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int APP_CONFIG_UNREGISTRATION_FAILURE = 3;
+}
diff --git a/android-34/android/bluetooth/BluetoothHealthAppConfiguration.java b/android-34/android/bluetooth/BluetoothHealthAppConfiguration.java
new file mode 100644
index 0000000..a84ecd9
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHealthAppConfiguration.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.bluetooth;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * The Bluetooth Health Application Configuration that is used in conjunction with
+ * the {@link BluetoothHealth} class. This class represents an application configuration
+ * that the Bluetooth Health third party application will register to communicate with the
+ * remote Bluetooth health device.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+@Deprecated
+public final class BluetoothHealthAppConfiguration implements Parcelable {
+
+ /**
+ * Hide auto-created default constructor
+ * @hide
+ */
+ BluetoothHealthAppConfiguration() {}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Return the data type associated with this application configuration.
+ *
+ * @return dataType
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public int getDataType() {
+ return 0;
+ }
+
+ /**
+ * Return the name of the application configuration.
+ *
+ * @return String name
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public String getName() {
+ return null;
+ }
+
+ /**
+ * Return the role associated with this application configuration.
+ *
+ * @return One of {@link BluetoothHealth#SOURCE_ROLE} or {@link BluetoothHealth#SINK_ROLE}
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public int getRole() {
+ return 0;
+ }
+
+ /**
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @NonNull
+ public static final Creator<BluetoothHealthAppConfiguration> CREATOR = new Creator<>() {
+ @Override
+ public BluetoothHealthAppConfiguration createFromParcel(Parcel in) {
+ return new BluetoothHealthAppConfiguration();
+ }
+
+ @Override
+ public BluetoothHealthAppConfiguration[] newArray(int size) {
+ return new BluetoothHealthAppConfiguration[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {}
+}
diff --git a/android-34/android/bluetooth/BluetoothHealthCallback.java b/android-34/android/bluetooth/BluetoothHealthCallback.java
new file mode 100644
index 0000000..4769212
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHealthCallback.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.bluetooth;
+
+import android.annotation.BinderThread;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+/**
+ * This abstract class is used to implement {@link BluetoothHealth} callbacks.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+@Deprecated
+public abstract class BluetoothHealthCallback {
+ private static final String TAG = "BluetoothHealthCallback";
+
+ /**
+ * Callback to inform change in registration state of the health
+ * application.
+ * <p> This callback is called on the binder thread (not on the UI thread)
+ *
+ * @param config Bluetooth Health app configuration
+ * @param status Success or failure of the registration or unregistration calls. Can be one of
+ * {@link BluetoothHealth#APP_CONFIG_REGISTRATION_SUCCESS} or {@link
+ * BluetoothHealth#APP_CONFIG_REGISTRATION_FAILURE} or
+ * {@link BluetoothHealth#APP_CONFIG_UNREGISTRATION_SUCCESS}
+ * or {@link BluetoothHealth#APP_CONFIG_UNREGISTRATION_FAILURE}
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @BinderThread
+ @Deprecated
+ public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config,
+ int status) {
+ Log.d(TAG, "onHealthAppConfigurationStatusChange: " + config + "Status: " + status);
+ }
+
+ /**
+ * Callback to inform change in channel state.
+ * <p> Its the responsibility of the implementor of this callback to close the
+ * parcel file descriptor when done. This callback is called on the Binder
+ * thread (not the UI thread)
+ *
+ * @param config The Health app configutation
+ * @param device The Bluetooth Device
+ * @param prevState The previous state of the channel
+ * @param newState The new state of the channel.
+ * @param fd The Parcel File Descriptor when the channel state is connected.
+ * @param channelId The id associated with the channel. This id will be used in future calls
+ * like when disconnecting the channel.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @BinderThread
+ @Deprecated
+ public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config,
+ BluetoothDevice device, int prevState, int newState, ParcelFileDescriptor fd,
+ int channelId) {
+ Log.d(TAG, "onHealthChannelStateChange: " + config + "Device: " + device
+ + "prevState:" + prevState + "newState:" + newState + "ParcelFd:" + fd
+ + "ChannelId:" + channelId);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothHearingAid.java b/android-34/android/bluetooth/BluetoothHearingAid.java
new file mode 100644
index 0000000..c6d8998
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHearingAid.java
@@ -0,0 +1,983 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Hearing Aid profile.
+ *
+ * <p>BluetoothHearingAid is a proxy object for controlling the Bluetooth Hearing Aid
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothHearingAid proxy object.
+ *
+ * <p> Android only supports one set of connected Bluetooth Hearing Aid device at a time. Each
+ * method is protected with its appropriate permission.
+ */
+public final class BluetoothHearingAid implements BluetoothProfile {
+ private static final String TAG = "BluetoothHearingAid";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * This class provides the APIs to get device's advertisement data. The advertisement data might
+ * be incomplete or not available.
+ *
+ * <p><a
+ * href=https://source.android.com/docs/core/connect/bluetooth/asha#advertisements-for-asha-gatt-service>
+ * documentation can be found here</a>
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final class AdvertisementServiceData implements Parcelable {
+ private static final String TAG = "AdvertisementData";
+
+ private final int mCapability;
+ private final int mTruncatedHiSyncId;
+
+ /**
+ * Construct AdvertisementServiceData.
+ *
+ * @param capability hearing aid's capability
+ * @param truncatedHiSyncId truncated HiSyncId
+ * @hide
+ */
+ public AdvertisementServiceData(int capability, int truncatedHiSyncId) {
+ if (DBG) {
+ Log.d(TAG, "capability:" + capability + " truncatedHiSyncId:" + truncatedHiSyncId);
+ }
+ mCapability = capability;
+ mTruncatedHiSyncId = truncatedHiSyncId;
+ }
+
+ /**
+ * Get the mode of the device based on its advertisement data.
+ *
+ * @hide
+ */
+ @RequiresPermission(
+ allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SystemApi
+ @DeviceMode
+ public int getDeviceMode() {
+ if (VDBG) log("getDeviceMode()");
+ return (mCapability >> 1) & 1;
+ }
+
+ private AdvertisementServiceData(@NonNull Parcel in) {
+ mCapability = in.readInt();
+ mTruncatedHiSyncId = in.readInt();
+ }
+
+ /**
+ * Get the side of the device based on its advertisement data.
+ *
+ * @hide
+ */
+ @RequiresPermission(
+ allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SystemApi
+ @DeviceSide
+ public int getDeviceSide() {
+ if (VDBG) log("getDeviceSide()");
+ return mCapability & 1;
+ }
+
+ /**
+ * Check if {@link BluetoothHearingAid} marks itself as CSIP supported based on its
+ * advertisement data.
+ *
+ * @return {@code true} when CSIP is supported, {@code false} otherwise
+ * @hide
+ */
+ @RequiresPermission(
+ allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SystemApi
+ public boolean isCsipSupported() {
+ if (VDBG) log("isCsipSupported()");
+ return ((mCapability >> 2) & 1) != 0;
+ }
+
+ /**
+ * Get the truncated HiSyncId of the device based on its advertisement data.
+ *
+ * @hide
+ */
+ @RequiresPermission(
+ allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SystemApi
+ public int getTruncatedHiSyncId() {
+ if (VDBG) log("getTruncatedHiSyncId: " + mTruncatedHiSyncId);
+ return mTruncatedHiSyncId;
+ }
+
+ /**
+ * Check if another {@link AdvertisementServiceData} is likely a pair with current one.
+ * There is a possibility of a collision on truncated HiSyncId which leads to falsely
+ * identified as a pair.
+ *
+ * @param data another device's {@link AdvertisementServiceData}
+ * @return {@code true} if the devices are a likely pair, {@code false} otherwise
+ * @hide
+ */
+ @RequiresPermission(
+ allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SystemApi
+ public boolean isInPairWith(@Nullable AdvertisementServiceData data) {
+ if (VDBG) log("isInPairWith()");
+ if (data == null) {
+ return false;
+ }
+
+ boolean bothSupportCsip = isCsipSupported() && data.isCsipSupported();
+ boolean isDifferentSide =
+ (getDeviceSide() != SIDE_UNKNOWN && data.getDeviceSide() != SIDE_UNKNOWN)
+ && (getDeviceSide() != data.getDeviceSide());
+ boolean isSameTruncatedHiSyncId = mTruncatedHiSyncId == data.mTruncatedHiSyncId;
+ return bothSupportCsip && isDifferentSide && isSameTruncatedHiSyncId;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mCapability);
+ dest.writeInt(mTruncatedHiSyncId);
+ }
+
+ public static final @NonNull Parcelable.Creator<AdvertisementServiceData> CREATOR =
+ new Parcelable.Creator<AdvertisementServiceData>() {
+ public AdvertisementServiceData createFromParcel(Parcel in) {
+ return new AdvertisementServiceData(in);
+ }
+
+ public AdvertisementServiceData[] newArray(int size) {
+ return new AdvertisementServiceData[size];
+ }
+ };
+
+ }
+
+ /**
+ * Intent used to broadcast the change in connection state of the Hearing Aid profile. Please
+ * note that in the binaural case, there will be two different LE devices for the left and right
+ * side and each device will have their own connection state changes.S
+ *
+ * <p>This intent will have 3 extras:
+ *
+ * <ul>
+ * <li>{@link #EXTRA_STATE} - The current state of the profile.
+ * <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
+ * <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link
+ * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link
+ * #STATE_DISCONNECTING}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.hearingaid.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the selection of a connected device as active.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SuppressLint("ActionValue")
+ public static final String ACTION_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.hearingaid.profile.action.ACTIVE_DEVICE_CHANGED";
+
+ /** @hide */
+ @IntDef(prefix = "SIDE_", value = {
+ SIDE_UNKNOWN,
+ SIDE_LEFT,
+ SIDE_RIGHT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeviceSide {}
+
+ /**
+ * Indicates the device side could not be read.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int SIDE_UNKNOWN = -1;
+
+ /**
+ * This device represents Left Hearing Aid.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int SIDE_LEFT = IBluetoothHearingAid.SIDE_LEFT;
+
+ /**
+ * This device represents Right Hearing Aid.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int SIDE_RIGHT = IBluetoothHearingAid.SIDE_RIGHT;
+
+ /** @hide */
+ @IntDef(prefix = "MODE_", value = {
+ MODE_UNKNOWN,
+ MODE_MONAURAL,
+ MODE_BINAURAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeviceMode {}
+
+ /**
+ * Indicates the device mode could not be read.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int MODE_UNKNOWN = -1;
+
+ /**
+ * This device is Monaural.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int MODE_MONAURAL = IBluetoothHearingAid.MODE_MONAURAL;
+
+ /**
+ * This device is Binaural (should receive only left or right audio).
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int MODE_BINAURAL = IBluetoothHearingAid.MODE_BINAURAL;
+
+ /**
+ * Indicates the HiSyncID could not be read and is unavailable.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final long HI_SYNC_ID_INVALID = 0;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHearingAid> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HEARING_AID,
+ "BluetoothHearingAid", IBluetoothHearingAid.class.getName()) {
+ @Override
+ public IBluetoothHearingAid getServiceInterface(IBinder service) {
+ return IBluetoothHearingAid.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothHearingAid proxy object for interacting with the local
+ * Bluetooth Hearing Aid service.
+ */
+ /* package */ BluetoothHearingAid(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothHearingAid getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is already connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that
+ * connection state intent for the profile will be broadcasted with
+ * the state. Users can get the connection state of the profile
+ * from this intent.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.connect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Initiate disconnection from a profile
+ *
+ * <p> This API will return false in scenarios like the profile on the
+ * Bluetooth device is not in connected state etc. When this API returns,
+ * true, it is guaranteed that the connection state change
+ * intent will be broadcasted with the state. Users can get the
+ * disconnection state of the profile from this intent.
+ *
+ * <p> If the disconnection is initiated by a remote device, the state
+ * will transition from {@link #STATE_CONNECTED} to
+ * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
+ * host (local) device the state will transition from
+ * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
+ * state {@link #STATE_DISCONNECTED}. The transition to
+ * {@link #STATE_DISCONNECTING} can be used to distinguish between the
+ * two scenarios.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.disconnect(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothHearingAid service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @NonNull
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothHearingAid service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @BluetoothProfile.BtProfileState
+ public int getConnectionState(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, Hearing Aid audio
+ * streaming is to the active Hearing Aid device. If a remote device
+ * is not connected, it cannot be selected as active.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ * @param device the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && ((device == null) || isValidDevice(device))) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setActiveDevice(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the connected physical Hearing Aid devices that are active
+ *
+ * @return the list of active devices. The first element is the left active
+ * device; the second element is the right active device. If either or both side
+ * is not active, it will be null on that position. Returns empty list on error.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull List<BluetoothDevice> getActiveDevices() {
+ if (VDBG) log("getActiveDevices()");
+ final IBluetoothHearingAid service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ SynchronousResultReceiver.get();
+ service.getActiveDevices(mAttributionSource, recv);
+ return Attributable.setAttributionSource(
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue),
+ mAttributionSource);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Set priority of the profile
+ *
+ * <p> The device should already be paired.
+ * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF},
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ if (DBG) log("setPriority(" + device + ", " + priority + ")");
+ return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority));
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true if connectionPolicy is set, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ verifyDeviceNotNull(device, "setConnectionPolicy");
+ final IBluetoothHearingAid service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)
+ && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
+ service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the priority of the profile.
+ *
+ * <p> The priority can be any of:
+ * {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public int getPriority(BluetoothDevice device) {
+ if (VDBG) log("getPriority(" + device + ")");
+ return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ verifyDeviceNotNull(device, "getConnectionPolicy");
+ final IBluetoothHearingAid service = getService();
+ final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getConnectionPolicy(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Helper for converting a state to a string.
+ *
+ * For debug use only - strings are not internationalized.
+ *
+ * @hide
+ */
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_DISCONNECTED:
+ return "disconnected";
+ case STATE_CONNECTING:
+ return "connecting";
+ case STATE_CONNECTED:
+ return "connected";
+ case STATE_DISCONNECTING:
+ return "disconnecting";
+ default:
+ return "<unknown state " + state + ">";
+ }
+ }
+
+ /**
+ * Tells remote device to set an absolute volume.
+ *
+ * @param volume Absolute volume to be set on remote
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void setVolume(int volume) {
+ if (DBG) Log.d(TAG, "setVolume(" + volume + ")");
+ final IBluetoothHearingAid service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
+ service.setVolume(volume, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Get the HiSyncId (unique hearing aid device identifier) of the device.
+ *
+ * <a href=https://source.android.com/devices/bluetooth/asha#hisyncid>HiSyncId documentation
+ * can be found here</a>
+ *
+ * @param device Bluetooth device
+ * @return the HiSyncId of the device
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public long getHiSyncId(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getHiSyncId(" + device + ")");
+ verifyDeviceNotNull(device, "getHiSyncId");
+ final IBluetoothHearingAid service = getService();
+ final long defaultValue = HI_SYNC_ID_INVALID;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Long> recv = SynchronousResultReceiver.get();
+ service.getHiSyncId(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the side of the device.
+ *
+ * @param device Bluetooth device.
+ * @return the {@code SIDE_LEFT}, {@code SIDE_RIGHT} of the device, or {@code SIDE_UNKNOWN} if
+ * one is not available.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @DeviceSide
+ public int getDeviceSide(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getDeviceSide(" + device + ")");
+ verifyDeviceNotNull(device, "getDeviceSide");
+ final IBluetoothHearingAid service = getService();
+ final int defaultValue = SIDE_UNKNOWN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getDeviceSide(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the mode of the device.
+ *
+ * @param device Bluetooth device
+ * @return the {@code MODE_MONAURAL}, {@code MODE_BINAURAL} of the device, or
+ * {@code MODE_UNKNOWN} if one is not available.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @DeviceMode
+ public int getDeviceMode(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getDeviceMode(" + device + ")");
+ verifyDeviceNotNull(device, "getDeviceMode");
+ final IBluetoothHearingAid service = getService();
+ final int defaultValue = MODE_UNKNOWN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getDeviceMode(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get ASHA device's advertisement service data.
+ *
+ * @param device discovered Bluetooth device
+ * @return {@link AdvertisementServiceData}
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(
+ allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Nullable AdvertisementServiceData getAdvertisementServiceData(
+ @NonNull BluetoothDevice device) {
+ if (DBG) {
+ log("getAdvertisementServiceData()");
+ }
+ final IBluetoothHearingAid service = getService();
+ AdvertisementServiceData result = null;
+ if (service == null || !isEnabled() || !isValidDevice(device)) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) {
+ log(Log.getStackTraceString(new Throwable()));
+ }
+ } else {
+ try {
+ final SynchronousResultReceiver<AdvertisementServiceData> recv =
+ SynchronousResultReceiver.get();
+ service.getAdvertisementServiceData(device, mAttributionSource, recv);
+ result = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Get the side of the device.
+ *
+ * <p>TODO(b/231901542): Used by internal only to improve hearing aids experience in short-term.
+ * Need to change to formal call in next bluetooth release.
+ *
+ * @param device Bluetooth device.
+ * @return SIDE_LEFT or SIDE_RIGHT
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private int getDeviceSideInternal(BluetoothDevice device) {
+ return getDeviceSide(device);
+ }
+
+ /**
+ * Get the mode of the device.
+ *
+ * <p>TODO(b/231901542): Used by internal only to improve hearing aids experience in short-term.
+ * Need to change to formal call in next bluetooth release.
+ *
+ * @param device Bluetooth device
+ * @return MODE_MONAURAL or MODE_BINAURAL
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private int getDeviceModeInternal(BluetoothDevice device) {
+ return getDeviceMode(device);
+ }
+
+ private boolean isEnabled() {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+ return false;
+ }
+
+ private void verifyDeviceNotNull(BluetoothDevice device, String methodName) {
+ if (device == null) {
+ Log.e(TAG, methodName + ": device param is null");
+ throw new IllegalArgumentException("Device cannot be null");
+ }
+ }
+
+ private boolean isValidDevice(BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/android-34/android/bluetooth/BluetoothHidDevice.java b/android-34/android/bluetooth/BluetoothHidDevice.java
new file mode 100644
index 0000000..0e39b3e
--- /dev/null
+++ b/android-34/android/bluetooth/BluetoothHidDevice.java
@@ -0,0 +1,850 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermi