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 -&gt; {
+ *      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 -&gt; {
+ *      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 -&gt; {
+ *      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}&lt;{@link AppSearchSession}&gt; 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}&lt;{@link GlobalSearchSession}&gt; 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&lt;Bundle&gt;, 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&mdash;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&mdash;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