• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.binder;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static com.google.common.base.Preconditions.checkState;
21 import static java.util.concurrent.TimeUnit.SECONDS;
22 
23 import android.app.Service;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.os.Binder;
27 import android.os.IBinder;
28 import android.os.Parcel;
29 import android.os.RemoteException;
30 import androidx.lifecycle.LifecycleService;
31 import com.google.auto.value.AutoValue;
32 import com.google.common.base.Supplier;
33 import com.google.common.collect.ImmutableList;
34 import io.grpc.NameResolver;
35 import io.grpc.Server;
36 import io.grpc.ServerServiceDefinition;
37 import io.grpc.ServerStreamTracer;
38 import io.grpc.binder.AndroidComponentAddress;
39 import io.grpc.internal.InternalServer;
40 import java.io.IOException;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.concurrent.CountDownLatch;
45 import java.util.concurrent.Executor;
46 import java.util.concurrent.ScheduledExecutorService;
47 import java.util.logging.Level;
48 import java.util.logging.Logger;
49 import javax.annotation.Nullable;
50 import javax.annotation.concurrent.GuardedBy;
51 
52 /**
53  * A test helper class for creating android services to host gRPC servers.
54  *
55  * <p>Currently only supports two servers at a time. If more are required, define a new class, add
56  * it to the manifest, and the hostServiceClasses array.
57  */
58 public final class HostServices {
59 
60   private static final Logger logger = Logger.getLogger(HostServices.class.getName());
61 
62   private static final Class<?>[] hostServiceClasses =
63       new Class<?>[] {
64         HostService1.class, HostService2.class,
65       };
66 
67 
68   public interface ServerFactory {
createServer(Service service, IBinderReceiver receiver)69     Server createServer(Service service, IBinderReceiver receiver);
70   }
71 
72   @AutoValue
73   public abstract static class ServiceParams {
74     @Nullable
transactionExecutor()75     abstract Executor transactionExecutor();
76 
77     @Nullable
rawBinderSupplier()78     abstract Supplier<IBinder> rawBinderSupplier();
79 
80     @Nullable
serverFactory()81     abstract ServerFactory serverFactory();
82 
toBuilder()83     public abstract Builder toBuilder();
84 
builder()85     public static Builder builder() {
86       return new AutoValue_HostServices_ServiceParams.Builder();
87     }
88 
89     @AutoValue.Builder
90     public abstract static class Builder {
setRawBinderSupplier(Supplier<IBinder> binderSupplier)91       public abstract Builder setRawBinderSupplier(Supplier<IBinder> binderSupplier);
92 
setServerFactory(ServerFactory serverFactory)93       public abstract Builder setServerFactory(ServerFactory serverFactory);
94 
95       /**
96        * If set, this executor will be used to pass any inbound transactions to the server. This can
97        * be used to simulate delayed, re-ordered, or dropped packets.
98        */
setTransactionExecutor(Executor transactionExecutor)99       public abstract Builder setTransactionExecutor(Executor transactionExecutor);
100 
build()101       public abstract ServiceParams build();
102     }
103   }
104 
105   @GuardedBy("HostServices.class")
106   private static final Map<Class<?>, AndroidComponentAddress> serviceAddresses = new HashMap<>();
107 
108   @GuardedBy("HostServices.class")
109   private static final Map<Class<?>, ServiceParams> serviceParams = new HashMap<>();
110 
111   @GuardedBy("HostServices.class")
112   private static final Map<Class<?>, HostService> activeServices = new HashMap<>();
113 
114   @Nullable
115   @GuardedBy("HostServices.class")
116   private static CountDownLatch serviceShutdownLatch;
117 
HostServices()118   private HostServices() {}
119 
120   /** Create a new {@link ServiceParams} builder. */
serviceParamsBuilder()121   public static ServiceParams.Builder serviceParamsBuilder() {
122     return ServiceParams.builder();
123   }
124 
125   /**
126    * Wait for all services to shutdown. This should be called from a test's tearDown method, to
127    * ensure the next test is able to use this class again (since Android itself is in control of the
128    * services).
129    */
awaitServiceShutdown()130   public static void awaitServiceShutdown() throws InterruptedException {
131     CountDownLatch latch = null;
132     synchronized (HostServices.class) {
133       if (serviceShutdownLatch == null && !activeServices.isEmpty()) {
134         latch = new CountDownLatch(activeServices.size());
135         serviceShutdownLatch = latch;
136       }
137       serviceParams.clear();
138       serviceAddresses.clear();
139     }
140     if (latch != null) {
141       if (!latch.await(10, SECONDS)) {
142         throw new AssertionError("Failed to shut down services");
143       }
144     }
145     synchronized (HostServices.class) {
146       checkState(activeServices.isEmpty());
147       checkState(serviceParams.isEmpty());
148       checkState(serviceAddresses.isEmpty());
149       serviceShutdownLatch = null;
150     }
151   }
152 
153   /** Create the address for a host-service. */
hostServiceAddress(Context appContext, Class<?> cls)154   private static AndroidComponentAddress hostServiceAddress(Context appContext, Class<?> cls) {
155     // NOTE: Even though we have a context object, we intentionally don't use a "local",
156     // address, since doing so would mark the address with our UID for security purposes,
157     // and that would limit the effectiveness of tests.
158     // Using this API forces us to rely on Binder.getCallingUid.
159     return AndroidComponentAddress.forRemoteComponent(appContext.getPackageName(), cls.getName());
160   }
161 
162   /**
163    * Allocate a new host service.
164    *
165    * @param appContext The application context.
166    * @return The AndroidComponentAddress of the service.
167    */
allocateService(Context appContext)168   public static synchronized AndroidComponentAddress allocateService(Context appContext) {
169     for (Class<?> cls : hostServiceClasses) {
170       if (!serviceAddresses.containsKey(cls)) {
171         AndroidComponentAddress address = hostServiceAddress(appContext, cls);
172         serviceAddresses.put(cls, address);
173         return address;
174       }
175     }
176     throw new AssertionError("This test helper only supports two services at a time.");
177   }
178 
179   /**
180    * Configure an allocated hosting service.
181    *
182    * @param androidComponentAddress The address of the service.
183    * @param params The parameters used to build the service.
184    */
configureService( AndroidComponentAddress androidComponentAddress, ServiceParams params)185   public static synchronized void configureService(
186       AndroidComponentAddress androidComponentAddress, ServiceParams params) {
187     for (Class<?> cls : hostServiceClasses) {
188       if (serviceAddresses.get(cls).equals(androidComponentAddress)) {
189         checkState(!serviceParams.containsKey(cls));
190         serviceParams.put(cls, params);
191         return;
192       }
193     }
194     throw new AssertionError("Unable to find service for address " + androidComponentAddress);
195   }
196 
197   /** An Android Service to host each gRPC server. */
198   private abstract static class HostService extends LifecycleService {
199 
200     @Nullable private ServiceParams params;
201     @Nullable private Supplier<IBinder> binderSupplier;
202     @Nullable private Server server;
203 
204     @Override
onCreate()205     public final void onCreate() {
206       super.onCreate();
207       Class<?> cls = getClass();
208       synchronized (HostServices.class) {
209         checkState(!activeServices.containsKey(cls));
210         activeServices.put(cls, this);
211         checkState(serviceParams.containsKey(cls));
212         params = serviceParams.get(cls);
213         ServerFactory factory = params.serverFactory();
214         if (factory != null) {
215           IBinderReceiver receiver = new IBinderReceiver();
216           server = factory.createServer(this, receiver);
217           try {
218             server.start();
219           } catch (IOException ioe) {
220             throw new AssertionError("Failed to start server", ioe);
221           }
222           binderSupplier = () -> receiver.get();
223         } else {
224           binderSupplier = params.rawBinderSupplier();
225           if (binderSupplier == null) {
226             throw new AssertionError("Insufficient params for host service");
227           }
228         }
229       }
230     }
231 
232     @Override
onBind(Intent intent)233     public final IBinder onBind(Intent intent) {
234       // Calling super here is a little weird (it returns null), but there's a @CallSuper
235       // annotation.
236       super.onBind(intent);
237       synchronized (HostServices.class) {
238         Executor executor = params.transactionExecutor();
239         if (executor != null) {
240           return new ProxyBinder(binderSupplier.get(), executor);
241         } else {
242           return binderSupplier.get();
243         }
244       }
245     }
246 
247     @Override
onDestroy()248     public final void onDestroy() {
249       synchronized (HostServices.class) {
250         if (server != null) {
251           server.shutdown();
252           server = null;
253         }
254         HostService removed = activeServices.remove(getClass());
255         checkState(removed == this);
256         serviceAddresses.remove(getClass());
257         serviceParams.remove(getClass());
258         if (serviceShutdownLatch != null) {
259           serviceShutdownLatch.countDown();
260         }
261       }
262       super.onDestroy();
263     }
264   }
265 
266   /** The first concrete host service */
267   public static final class HostService1 extends HostService {}
268 
269   /** The second concrete host service */
270   public static final class HostService2 extends HostService {}
271 
272   /** Wraps an IBinder to send incoming transactions to a different thread. */
273   private static class ProxyBinder extends Binder {
274     private final IBinder delegate;
275     private final Executor executor;
276 
ProxyBinder(IBinder delegate, Executor executor)277     ProxyBinder(IBinder delegate, Executor executor) {
278       this.delegate = delegate;
279       this.executor = executor;
280     }
281 
282     @Override
onTransact(int code, Parcel parcel, Parcel reply, int flags)283     protected boolean onTransact(int code, Parcel parcel, Parcel reply, int flags) {
284       executor.execute(
285           () -> {
286             try {
287               delegate.transact(code, parcel, reply, flags);
288             } catch (RemoteException re) {
289               logger.log(Level.WARNING, "Exception in proxybinder", re);
290             }
291           });
292       return true;
293     }
294   }
295 }
296