diff --git a/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicies.java b/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicies.java
new file mode 100644
index 00000000000..bd75d5bdec3
--- /dev/null
+++ b/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicies.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2026 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.grpc.binder;
+
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.errorprone.annotations.CheckReturnValue;
+import io.grpc.ExperimentalApi;
+import io.grpc.Status;
+import java.util.concurrent.Executor;
+
+/** Static factory methods for creating asynchronous security policies. */
+@CheckReturnValue
+public final class AsyncSecurityPolicies {
+
+ private AsyncSecurityPolicies() {}
+
+ /**
+ * Adapts a future {@link AsyncSecurityPolicy} into an instance of {@link AsyncSecurityPolicy}
+ * that can be used right away.
+ *
+ *
Use this when your actual security policy is loaded asynchronously (e.g., from disk or
+ * network) and is not immediately available at gRPC channel or server initialization. For
+ * example, Android IPC servers can't defer "listening" while some slow or async initialization
+ * process completes. Instead, Android *tells* a server to initialize itself just-in-time for the
+ * first client connection. And this instruction arrives as a callback to {@code
+ * Service#onCreate()} then {@code Service#onBind()} on the app's main thread, where blocking to
+ * load a security policy would risk an "Application Not Responding" (ANR) error.
+ *
+ *
The returned policy will defer all authorization decisions until {@code futurePolicy}
+ * completes. So any calls or connections needing authorization will wait. After the future
+ * completes, all new and pending authorization checks are delegated to the loaded policy. If
+ * {@code futurePolicy} completes with an exception, all authorization checks will fail.
+ *
+ * @param futurePolicy The future that will resolve to the delegate security policy.
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
+ public static AsyncSecurityPolicy fromFuture(
+ final ListenableFuture extends AsyncSecurityPolicy> futurePolicy) {
+ Preconditions.checkNotNull(futurePolicy, "futurePolicy");
+ return new AsyncSecurityPolicy() {
+ @Override
+ public ListenableFuture checkAuthorizationAsync(int uid) {
+ return Futures.transformAsync(
+ Futures.nonCancellationPropagating(futurePolicy),
+ policy -> policy.checkAuthorizationAsync(uid),
+ MoreExecutors.directExecutor());
+ }
+ };
+ }
+
+ /**
+ * Adapts a future synchronous {@link SecurityPolicy} into an {@link AsyncSecurityPolicy}.
+ *
+ * See {@link #fromFuture(ListenableFuture)} for details on the use case.
+ *
+ *
Because the delegate policy is synchronous and may perform blocking operations (like disk
+ * I/O), the actual authorization checks are offloaded to the provided {@code offloadExecutor}.
+ *
+ * @param futurePolicy The future that will resolve to the delegate security policy.
+ * @param offloadExecutor The executor on which to run the delegate's blocking {@link
+ * SecurityPolicy#checkAuthorization} call.
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
+ public static AsyncSecurityPolicy fromFuture(
+ final ListenableFuture extends SecurityPolicy> futurePolicy, Executor offloadExecutor) {
+ Preconditions.checkNotNull(futurePolicy, "futurePolicy");
+ return new AsyncSecurityPolicy() {
+ @Override
+ public ListenableFuture checkAuthorizationAsync(int uid) {
+ return Futures.transform(
+ Futures.nonCancellationPropagating(futurePolicy),
+ policy -> policy.checkAuthorization(uid),
+ offloadExecutor);
+ }
+ };
+ }
+}
diff --git a/binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java b/binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java
new file mode 100644
index 00000000000..f96e8458db7
--- /dev/null
+++ b/binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2026 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.grpc.binder;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import android.os.Process;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import io.grpc.Status;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class AsyncSecurityPoliciesTest {
+
+ private static final int MY_UID = Process.myUid();
+
+ @Test
+ public void testFromFuture_asyncPolicy_succeeds() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture.isDone()).isFalse();
+
+ AsyncSecurityPolicy delegatePolicy =
+ new AsyncSecurityPolicy() {
+ @Override
+ public ListenableFuture checkAuthorizationAsync(int uid) {
+ return Futures.immediateFuture(Status.OK.withDescription("yay"));
+ }
+ };
+
+ futurePolicy.set(delegatePolicy);
+
+ assertThat(authFuture.isDone()).isTrue();
+ assertThat(authFuture.get().getDescription()).isEqualTo("yay");
+ }
+
+ @Test
+ public void testFromFuture_asyncPolicy_fails() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture.isDone()).isFalse();
+
+ AsyncSecurityPolicy delegatePolicy =
+ new AsyncSecurityPolicy() {
+ @Override
+ public ListenableFuture checkAuthorizationAsync(int uid) {
+ return Futures.immediateFuture(Status.PERMISSION_DENIED.withDescription("ouch"));
+ }
+ };
+
+ futurePolicy.set(delegatePolicy);
+
+ assertThat(authFuture.isDone()).isTrue();
+ assertThat(authFuture.get().getDescription()).isEqualTo("ouch");
+ }
+
+ @Test
+ public void testFromFuture_asyncPolicy_futureFails() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture.isDone()).isFalse();
+
+ Exception exception = new RuntimeException("failed to load policy");
+ futurePolicy.setException(exception);
+
+ assertThat(authFuture.isDone()).isTrue();
+ try {
+ authFuture.get();
+ fail("Expected ExecutionException");
+ } catch (java.util.concurrent.ExecutionException e) {
+ assertThat(e.getCause()).isEqualTo(exception);
+ }
+ }
+
+ @Test
+ public void testFromFuture_syncPolicy_succeeds() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture.isDone()).isFalse();
+
+ SecurityPolicy delegatePolicy =
+ new SecurityPolicy() {
+ @Override
+ public Status checkAuthorization(int uid) {
+ return Status.OK.withDescription("yay");
+ }
+ };
+
+ futurePolicy.set(delegatePolicy);
+
+ assertThat(authFuture.isDone()).isTrue();
+ assertThat(authFuture.get().getDescription()).isEqualTo("yay");
+ }
+
+ @Test
+ public void testFromFuture_syncPolicy_fails() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture.isDone()).isFalse();
+
+ SecurityPolicy delegatePolicy =
+ new SecurityPolicy() {
+ @Override
+ public Status checkAuthorization(int uid) {
+ return Status.PERMISSION_DENIED.withDescription("ouch");
+ }
+ };
+
+ futurePolicy.set(delegatePolicy);
+
+ assertThat(authFuture.isDone()).isTrue();
+ assertThat(authFuture.get().getDescription()).isEqualTo("ouch");
+ }
+
+ @Test
+ public void testFromFuture_syncPolicy_futureFails() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture.isDone()).isFalse();
+
+ Exception exception = new RuntimeException("failed to load policy");
+ futurePolicy.setException(exception);
+
+ assertThat(authFuture.isDone()).isTrue();
+ try {
+ authFuture.get();
+ fail("Expected ExecutionException");
+ } catch (java.util.concurrent.ExecutionException e) {
+ assertThat(e.getCause()).isEqualTo(exception);
+ }
+ }
+
+ @Test
+ public void testFromFuture_syncPolicy_usesExecutor() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AtomicInteger executeCount = new AtomicInteger(0);
+ Executor executor =
+ r -> {
+ executeCount.incrementAndGet();
+ r.run();
+ };
+ AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy, executor);
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ SecurityPolicy delegatePolicy =
+ new SecurityPolicy() {
+ @Override
+ public Status checkAuthorization(int uid) {
+ return Status.OK.withDescription("yay");
+ }
+ };
+ futurePolicy.set(delegatePolicy);
+
+ assertThat(authFuture.get().getDescription()).isEqualTo("yay");
+ assertThat(executeCount.get()).isEqualTo(1);
+ }
+
+ @Test
+ public void testFromFuture_asyncPolicy_cancellationDoesNotPropagateBackwards() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ authFuture.cancel(true);
+
+ assertThat(futurePolicy.isCancelled()).isFalse();
+ }
+
+ @Test
+ public void testFromFuture_syncPolicy_cancellationDoesNotPropagateBackwards() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ authFuture.cancel(true);
+
+ assertThat(futurePolicy.isCancelled()).isFalse();
+ }
+
+ @Test
+ public void testFromFuture_asyncPolicy_cancellationDoesNotPoisonSubsequentCalls()
+ throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);
+
+ ListenableFuture authFuture1 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ authFuture1.cancel(true);
+
+ // Now try again
+ ListenableFuture authFuture2 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+
+ AsyncSecurityPolicy delegatePolicy =
+ new AsyncSecurityPolicy() {
+ @Override
+ public ListenableFuture checkAuthorizationAsync(int uid) {
+ return Futures.immediateFuture(Status.OK.withDescription("yay"));
+ }
+ };
+ futurePolicy.set(delegatePolicy);
+
+ assertThat(authFuture2.isDone()).isTrue();
+ assertThat(authFuture2.get().getDescription()).isEqualTo("yay");
+ }
+
+ @Test
+ public void testFromFuture_syncPolicy_cancellationDoesNotPoisonSubsequentCalls()
+ throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());
+
+ ListenableFuture authFuture1 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ authFuture1.cancel(true);
+
+ // Now try again
+ ListenableFuture authFuture2 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+
+ SecurityPolicy delegatePolicy =
+ new SecurityPolicy() {
+ @Override
+ public Status checkAuthorization(int uid) {
+ return Status.OK.withDescription("yay");
+ }
+ };
+ futurePolicy.set(delegatePolicy);
+
+ assertThat(authFuture2.isDone()).isTrue();
+ assertThat(authFuture2.get().getDescription()).isEqualTo("yay");
+ }
+}