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..a1631a76364
--- /dev/null
+++ b/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicies.java
@@ -0,0 +1,92 @@
+/*
+ * 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.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.AsyncCallable;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+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() {}
+
+ /**
+ * Returns an {@link AsyncSecurityPolicy} that delegates to some other policy that's provided
+ * lazily and asynchronously.
+ *
+ *
Use this when your security policy is slow or expensive to load and not immediately
+ * available at Channel or Server initialization. It's particularly useful in {@code
+ * android.app.Service#onCreate()} where blocking the main thread to load a server's security
+ * policy risks an "Application Not Responding" (ANR) error. Implementations of 'policyProvider'
+ * must not block the calling thread either.
+ *
+ *
The provided {@link AsyncCallable} is invoked each time the returned policy is evaluated.
+ * This happens once per connection for a grpc-binder Channel and once per (service, incoming
+ * connection) called on a Server. So 'policyProvider' must be prepared to be invoked more than
+ * once but not normally for every RPC. Depending on the cost of loading the policy, a provider
+ * may want to memoize and reuse its products.
+ *
+ *
Binder Channels and Servers try to coalesce multiple identical authorization checks that
+ * overlap in time but this isn't guaranteed. So 'policyProvider' must be thread-safe but may or
+ * may not go to the trouble of coalescing simultaneous calls for itself. Those that do should use
+ * {@link Futures#nonCancellationPropagating} or similar to protect a future returned to multiple
+ * callers from individual cancellation.
+ *
+ *
'policyProvider' can express the failure to load a security policy by returning a failed
+ * future. This failure will propagate to the Server or Channel operation that needed authorizing,
+ * but will not be cached, leaving open the possibility of success upon retry. A memoizing policy
+ * provider may want to retain only successes for the same reason.
+ *
+ * @param policyProvider used to get the delegate SecurityPolicy when needed
+ * @param executor used to call into the delegate once provided. If the delegate is a
+ * SecurityPolicy, note that many implementations of checkAuthorization() block.
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
+ public static AsyncSecurityPolicy deferredAsync(
+ AsyncCallable policyProvider, Executor executor) {
+ checkNotNull(policyProvider, "policyProvider");
+ checkNotNull(executor, "executor");
+ return new AsyncSecurityPolicy() {
+ @Override
+ public ListenableFuture checkAuthorizationAsync(int uid) {
+ try {
+ return Futures.transformAsync(
+ policyProvider.call(),
+ policy -> {
+ checkNotNull(policy, "policyProvider returned a null SecurityPolicy");
+ if (policy instanceof AsyncSecurityPolicy) {
+ return ((AsyncSecurityPolicy) policy).checkAuthorizationAsync(uid);
+ }
+ return Futures.immediateFuture(policy.checkAuthorization(uid));
+ },
+ executor);
+ } catch (Exception e) {
+ return Futures.immediateFailedFuture(e);
+ }
+ }
+ };
+ }
+}
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..db379de9d18
--- /dev/null
+++ b/binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java
@@ -0,0 +1,325 @@
+/*
+ * 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.SettableFuture;
+import io.grpc.Status;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.After;
+import org.junit.Before;
+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();
+
+ private ExecutorService executor;
+
+ @Before
+ public void setUp() {
+ executor = Executors.newSingleThreadExecutor();
+ }
+
+ @After
+ public void tearDown() {
+ executor.shutdown();
+ }
+
+ @Test
+ public void testDeferredAsync_asyncPolicy_succeeds() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(() -> futurePolicy, executor);
+
+ 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.get().getDescription()).isEqualTo("yay");
+ }
+
+ @Test
+ public void testDeferredAsync_asyncPolicy_fails() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(() -> futurePolicy, executor);
+
+ 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.get().getDescription()).isEqualTo("ouch");
+ }
+
+ @Test
+ public void testDeferredAsync_policyProviderReturnsFailedFuture() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(() -> futurePolicy, executor);
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture.isDone()).isFalse();
+
+ Exception exception = new RuntimeException("failed to load policy");
+ futurePolicy.setException(exception);
+
+ try {
+ authFuture.get();
+ fail("Expected ExecutionException");
+ } catch (ExecutionException e) {
+ assertThat(e.getCause()).isEqualTo(exception);
+ }
+ }
+
+ @Test
+ public void testDeferredAsync_policyProviderThrowsException() throws Exception {
+ Exception exception = new RuntimeException("failed to call callable");
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(
+ () -> {
+ throw exception;
+ },
+ executor);
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+
+ try {
+ authFuture.get();
+ fail("Expected ExecutionException");
+ } catch (ExecutionException e) {
+ assertThat(e.getCause()).isEqualTo(exception);
+ }
+ }
+
+ @Test
+ public void testDeferredAsync_syncPolicy_succeeds() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(() -> futurePolicy, executor);
+
+ 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.get().getDescription()).isEqualTo("yay");
+ }
+
+ @Test
+ public void testDeferredAsync_syncPolicy_fails() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(() -> futurePolicy, executor);
+
+ 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.get().getDescription()).isEqualTo("ouch");
+ }
+
+ @Test
+ public void testDeferredAsync_syncPolicy_usesExecutor() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AtomicInteger executeCount = new AtomicInteger(0);
+ Executor customExecutor =
+ r -> {
+ executeCount.incrementAndGet();
+ executor.execute(r);
+ };
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(() -> futurePolicy, customExecutor);
+
+ 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 testDeferredAsync_invokesCallableOnEachCheck() throws Exception {
+ AtomicInteger callCount = new AtomicInteger(0);
+ SecurityPolicy delegatePolicy =
+ new SecurityPolicy() {
+ @Override
+ public Status checkAuthorization(int uid) {
+ return Status.OK.withDescription("yay");
+ }
+ };
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(
+ () -> {
+ callCount.incrementAndGet();
+ return Futures.immediateFuture(delegatePolicy);
+ },
+ executor);
+
+ ListenableFuture authFuture1 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture1.get().getDescription()).isEqualTo("yay");
+
+ ListenableFuture authFuture2 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture2.get().getDescription()).isEqualTo("yay");
+
+ assertThat(callCount.get()).isEqualTo(2);
+ }
+
+ @Test
+ public void testDeferredAsync_retriesAfterFailure() throws Exception {
+ AtomicInteger callCount = new AtomicInteger(0);
+ SecurityPolicy delegatePolicy =
+ new SecurityPolicy() {
+ @Override
+ public Status checkAuthorization(int uid) {
+ return Status.OK.withDescription("yay");
+ }
+ };
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(
+ () -> {
+ if (callCount.getAndIncrement() == 0) {
+ return Futures.immediateFailedFuture(
+ new RuntimeException("first attempt failed"));
+ }
+ return Futures.immediateFuture(delegatePolicy);
+ },
+ executor);
+
+ ListenableFuture authFuture1 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ try {
+ authFuture1.get();
+ fail("Expected ExecutionException");
+ } catch (ExecutionException e) {
+ assertThat(e.getCause().getMessage()).isEqualTo("first attempt failed");
+ }
+
+ ListenableFuture authFuture2 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture2.get().getDescription()).isEqualTo("yay");
+ assertThat(callCount.get()).isEqualTo(2);
+ }
+
+ @Test
+ public void testDeferredAsync_cancellationDoesNotPropagateBackwards() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(
+ () -> Futures.nonCancellationPropagating(futurePolicy), executor);
+
+ ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ authFuture.cancel(true);
+
+ assertThat(futurePolicy.isCancelled()).isFalse();
+ }
+
+ @Test
+ public void testDeferredAsync_cancellationDoesNotPoisonSubsequentCalls() throws Exception {
+ SettableFuture futurePolicy = SettableFuture.create();
+ AsyncSecurityPolicy asyncPolicy =
+ AsyncSecurityPolicies.deferredAsync(
+ () -> Futures.nonCancellationPropagating(futurePolicy), executor);
+
+ ListenableFuture authFuture1 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ authFuture1.cancel(true);
+
+ SecurityPolicy delegatePolicy =
+ new SecurityPolicy() {
+ @Override
+ public Status checkAuthorization(int uid) {
+ return Status.OK.withDescription("yay");
+ }
+ };
+ futurePolicy.set(delegatePolicy);
+
+ ListenableFuture authFuture2 = asyncPolicy.checkAuthorizationAsync(MY_UID);
+ assertThat(authFuture2.get().getDescription()).isEqualTo("yay");
+ }
+
+ @Test
+ public void testDeferredAsync_nullProvider_throwsException() {
+ try {
+ AsyncSecurityPolicies.deferredAsync(null, executor);
+ fail("Expected NullPointerException");
+ } catch (NullPointerException e) {
+ assertThat(e).hasMessageThat().contains("policyProvider");
+ }
+ }
+
+ @Test
+ public void testDeferredAsync_nullExecutor_throwsException() {
+ try {
+ AsyncSecurityPolicies.deferredAsync(() -> Futures.immediateFuture(null), null);
+ fail("Expected NullPointerException");
+ } catch (NullPointerException e) {
+ assertThat(e).hasMessageThat().contains("executor");
+ }
+ }
+}