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