1 // Copyright 2019 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net.test; 6 7 import android.content.Context; 8 9 import androidx.annotation.GuardedBy; 10 11 import org.chromium.net.BidirectionalStream; 12 import org.chromium.net.CronetEngine; 13 import org.chromium.net.ExperimentalBidirectionalStream; 14 import org.chromium.net.NetworkQualityRttListener; 15 import org.chromium.net.NetworkQualityThroughputListener; 16 import org.chromium.net.RequestFinishedInfo; 17 import org.chromium.net.UrlRequest; 18 import org.chromium.net.impl.CronetEngineBase; 19 import org.chromium.net.impl.CronetEngineBuilderImpl; 20 import org.chromium.net.impl.ImplVersion; 21 import org.chromium.net.impl.RefCountDelegate; 22 import org.chromium.net.impl.UrlRequestBase; 23 import org.chromium.net.impl.VersionSafeCallbacks; 24 25 import java.io.IOException; 26 import java.net.Proxy; 27 import java.net.URL; 28 import java.net.URLConnection; 29 import java.net.URLStreamHandlerFactory; 30 import java.util.Collection; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.concurrent.Executor; 35 import java.util.concurrent.ExecutorService; 36 import java.util.concurrent.Executors; 37 import java.util.concurrent.LinkedBlockingQueue; 38 import java.util.concurrent.ThreadFactory; 39 import java.util.concurrent.ThreadPoolExecutor; 40 import java.util.concurrent.TimeUnit; 41 42 /** Fake {@link CronetEngine}. This implements CronetEngine. */ 43 final class FakeCronetEngine extends CronetEngineBase { 44 /** Builds a {@link FakeCronetEngine}. This implements CronetEngine.Builder. */ 45 static class Builder extends CronetEngineBuilderImpl { 46 private FakeCronetController mController; 47 48 /** 49 * Builder for {@link FakeCronetEngine}. 50 * 51 * @param context Android {@link Context}. 52 */ Builder(Context context)53 Builder(Context context) { 54 super(context); 55 } 56 57 @Override build()58 public FakeCronetEngine build() { 59 return new FakeCronetEngine(this); 60 } 61 setController(FakeCronetController controller)62 void setController(FakeCronetController controller) { 63 mController = controller; 64 } 65 } 66 67 private final FakeCronetController mController; 68 private final ExecutorService mExecutorService; 69 70 private final Object mLock = new Object(); 71 72 @GuardedBy("mLock") 73 private boolean mIsShutdown; 74 75 /** 76 * The number of started requests where the terminal callback (i.e. 77 * onSucceeded/onCancelled/onFailed) has not yet been called. 78 */ 79 @GuardedBy("mLock") 80 private int mRunningRequestCount; 81 82 /* 83 * The number of started requests where the terminal callbacks (i.e. 84 * onSucceeded/onCancelled/onFailed, request finished listeners) have not 85 * all returned yet. 86 * 87 * By definition this is always greater than or equal to 88 * mRunningRequestCount. The difference between the two is the number of 89 * terminal callbacks that are currently running. 90 */ 91 @GuardedBy("mLock") 92 private int mActiveRequestCount; 93 94 @GuardedBy("mLock") 95 private final Map< 96 RequestFinishedInfo.Listener, VersionSafeCallbacks.RequestFinishedInfoListener> 97 mFinishedListenerMap = new HashMap<>(); 98 99 /** 100 * Creates a {@link FakeCronetEngine}. Used when {@link FakeCronetEngine} is created with the 101 * {@link FakeCronetEngine.Builder}. 102 * 103 * @param builder a {@link CronetEngineBuilderImpl} to build this {@link CronetEngine} 104 * implementation from. 105 */ FakeCronetEngine(FakeCronetEngine.Builder builder)106 private FakeCronetEngine(FakeCronetEngine.Builder builder) { 107 if (builder.mController != null) { 108 mController = builder.mController; 109 } else { 110 mController = new FakeCronetController(); 111 } 112 mExecutorService = 113 new ThreadPoolExecutor( 114 /* corePoolSize= */ 1, 115 /* maximumPoolSize= */ 5, 116 /* keepAliveTime= */ 50, 117 TimeUnit.SECONDS, 118 new LinkedBlockingQueue<Runnable>(), 119 new ThreadFactory() { 120 @Override 121 public Thread newThread(final Runnable r) { 122 return Executors.defaultThreadFactory() 123 .newThread( 124 () -> { 125 Thread.currentThread() 126 .setName("FakeCronetEngine"); 127 r.run(); 128 }); 129 } 130 }); 131 FakeCronetController.addFakeCronetEngine(this); 132 } 133 134 /** 135 * Gets the controller associated with this instance that will be used for responses to 136 * {@link UrlRequest}s. 137 * 138 * @return the {@link FakeCronetCntroller} that controls this {@link FakeCronetEngine}. 139 */ getController()140 FakeCronetController getController() { 141 return mController; 142 } 143 144 @Override newBidirectionalStreamBuilder( String url, BidirectionalStream.Callback callback, Executor executor)145 public ExperimentalBidirectionalStream.Builder newBidirectionalStreamBuilder( 146 String url, BidirectionalStream.Callback callback, Executor executor) { 147 synchronized (mLock) { 148 if (mIsShutdown) { 149 throw new IllegalStateException( 150 "This instance of CronetEngine has been shutdown and can no longer be " 151 + "used."); 152 } 153 throw new UnsupportedOperationException( 154 "The bidirectional stream API is not supported by the Fake implementation " 155 + "of CronetEngine."); 156 } 157 } 158 159 @Override getVersionString()160 public String getVersionString() { 161 return "FakeCronet/" + ImplVersion.getCronetVersionWithLastChange(); 162 } 163 164 @Override shutdown()165 public void shutdown() { 166 synchronized (mLock) { 167 if (mRunningRequestCount != 0) { 168 throw new IllegalStateException("Cannot shutdown with running requests."); 169 } else { 170 mIsShutdown = true; 171 } 172 } 173 mExecutorService.shutdown(); 174 FakeCronetController.removeFakeCronetEngine(this); 175 } 176 177 @Override startNetLogToFile(String fileName, boolean logAll)178 public void startNetLogToFile(String fileName, boolean logAll) {} 179 180 @Override startNetLogToDisk(String dirPath, boolean logAll, int maxSize)181 public void startNetLogToDisk(String dirPath, boolean logAll, int maxSize) {} 182 183 @Override stopNetLog()184 public void stopNetLog() {} 185 186 @Override getGlobalMetricsDeltas()187 public byte[] getGlobalMetricsDeltas() { 188 return new byte[0]; 189 } 190 191 @Override getEffectiveConnectionType()192 public int getEffectiveConnectionType() { 193 return EFFECTIVE_CONNECTION_TYPE_UNKNOWN; 194 } 195 196 @Override getHttpRttMs()197 public int getHttpRttMs() { 198 return CONNECTION_METRIC_UNKNOWN; 199 } 200 201 @Override getTransportRttMs()202 public int getTransportRttMs() { 203 return CONNECTION_METRIC_UNKNOWN; 204 } 205 206 @Override getDownstreamThroughputKbps()207 public int getDownstreamThroughputKbps() { 208 return CONNECTION_METRIC_UNKNOWN; 209 } 210 211 @Override bindToNetwork(long networkHandle)212 public void bindToNetwork(long networkHandle) { 213 throw new UnsupportedOperationException( 214 "The multi-network API is not supported by the Fake implementation " 215 + "of Cronet Engine"); 216 } 217 218 @Override configureNetworkQualityEstimatorForTesting( boolean useLocalHostRequests, boolean useSmallerResponses, boolean disableOfflineCheck)219 public void configureNetworkQualityEstimatorForTesting( 220 boolean useLocalHostRequests, 221 boolean useSmallerResponses, 222 boolean disableOfflineCheck) {} 223 224 @Override addRttListener(NetworkQualityRttListener listener)225 public void addRttListener(NetworkQualityRttListener listener) {} 226 227 @Override removeRttListener(NetworkQualityRttListener listener)228 public void removeRttListener(NetworkQualityRttListener listener) {} 229 230 @Override addThroughputListener(NetworkQualityThroughputListener listener)231 public void addThroughputListener(NetworkQualityThroughputListener listener) {} 232 233 @Override removeThroughputListener(NetworkQualityThroughputListener listener)234 public void removeThroughputListener(NetworkQualityThroughputListener listener) {} 235 236 @Override addRequestFinishedListener(RequestFinishedInfo.Listener listener)237 public void addRequestFinishedListener(RequestFinishedInfo.Listener listener) { 238 if (listener == null) { 239 throw new IllegalArgumentException("Listener must not be null"); 240 } 241 synchronized (mLock) { 242 mFinishedListenerMap.put( 243 listener, new VersionSafeCallbacks.RequestFinishedInfoListener(listener)); 244 } 245 } 246 247 @Override removeRequestFinishedListener(RequestFinishedInfo.Listener listener)248 public void removeRequestFinishedListener(RequestFinishedInfo.Listener listener) { 249 if (listener == null) { 250 throw new IllegalArgumentException("Listener must not be null"); 251 } 252 synchronized (mLock) { 253 mFinishedListenerMap.remove(listener); 254 } 255 } 256 hasRequestFinishedListeners()257 boolean hasRequestFinishedListeners() { 258 synchronized (mLock) { 259 return !mFinishedListenerMap.isEmpty(); 260 } 261 } 262 reportRequestFinished( RequestFinishedInfo requestInfo, RefCountDelegate inflightDoneCallbackCount)263 void reportRequestFinished( 264 RequestFinishedInfo requestInfo, RefCountDelegate inflightDoneCallbackCount) { 265 synchronized (mLock) { 266 for (RequestFinishedInfo.Listener listener : mFinishedListenerMap.values()) { 267 inflightDoneCallbackCount.increment(); 268 listener.getExecutor() 269 .execute( 270 () -> { 271 try { 272 listener.onRequestFinished(requestInfo); 273 } finally { 274 inflightDoneCallbackCount.decrement(); 275 } 276 }); 277 } 278 } 279 } 280 281 // TODO(crbug.com/669707) Instantiate a fake CronetHttpUrlConnection wrapping a FakeUrlRequest 282 // here. 283 @Override openConnection(URL url)284 public URLConnection openConnection(URL url) throws IOException { 285 throw new UnsupportedOperationException( 286 "The openConnection API is not supported by the Fake implementation of " 287 + "CronetEngine."); 288 } 289 290 @Override openConnection(URL url, Proxy proxy)291 public URLConnection openConnection(URL url, Proxy proxy) throws IOException { 292 throw new UnsupportedOperationException( 293 "The openConnection API is not supported by the Fake implementation of " 294 + "CronetEngine."); 295 } 296 297 @Override createURLStreamHandlerFactory()298 public URLStreamHandlerFactory createURLStreamHandlerFactory() { 299 throw new UnsupportedOperationException( 300 "The URLStreamHandlerFactory API is not supported by the Fake implementation of " 301 + "CronetEngine."); 302 } 303 304 @Override createRequest( String url, UrlRequest.Callback callback, Executor userExecutor, int priority, Collection<Object> connectionAnnotations, boolean disableCache, boolean disableConnectionMigration, boolean allowDirectExecutor, boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, int trafficStatsUid, RequestFinishedInfo.Listener requestFinishedListener, int idempotency, long networkHandle)305 protected UrlRequestBase createRequest( 306 String url, 307 UrlRequest.Callback callback, 308 Executor userExecutor, 309 int priority, 310 Collection<Object> connectionAnnotations, 311 boolean disableCache, 312 boolean disableConnectionMigration, 313 boolean allowDirectExecutor, 314 boolean trafficStatsTagSet, 315 int trafficStatsTag, 316 boolean trafficStatsUidSet, 317 int trafficStatsUid, 318 RequestFinishedInfo.Listener requestFinishedListener, 319 int idempotency, 320 long networkHandle) { 321 if (networkHandle != DEFAULT_NETWORK_HANDLE) { 322 throw new UnsupportedOperationException( 323 "The multi-network API is not supported by the Fake implementation " 324 + "of Cronet Engine"); 325 } 326 327 synchronized (mLock) { 328 if (mIsShutdown) { 329 throw new IllegalStateException( 330 "This instance of CronetEngine has been shutdown and can no longer be " 331 + "used."); 332 } 333 return new FakeUrlRequest( 334 callback, 335 userExecutor, 336 mExecutorService, 337 url, 338 allowDirectExecutor, 339 trafficStatsTagSet, 340 trafficStatsTag, 341 trafficStatsUidSet, 342 trafficStatsUid, 343 mController, 344 this, 345 connectionAnnotations); 346 } 347 } 348 349 @Override getActiveRequestCount()350 public int getActiveRequestCount() { 351 synchronized (mLock) { 352 return mActiveRequestCount; 353 } 354 } 355 356 @Override createBidirectionalStream( String url, BidirectionalStream.Callback callback, Executor executor, String httpMethod, List<Map.Entry<String, String>> requestHeaders, @StreamPriority int priority, boolean delayRequestHeadersUntilFirstFlush, Collection<Object> connectionAnnotations, boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, int trafficStatsUid, long networkHandle)357 protected ExperimentalBidirectionalStream createBidirectionalStream( 358 String url, 359 BidirectionalStream.Callback callback, 360 Executor executor, 361 String httpMethod, 362 List<Map.Entry<String, String>> requestHeaders, 363 @StreamPriority int priority, 364 boolean delayRequestHeadersUntilFirstFlush, 365 Collection<Object> connectionAnnotations, 366 boolean trafficStatsTagSet, 367 int trafficStatsTag, 368 boolean trafficStatsUidSet, 369 int trafficStatsUid, 370 long networkHandle) { 371 if (networkHandle != DEFAULT_NETWORK_HANDLE) { 372 throw new UnsupportedOperationException( 373 "The multi-network API is not supported by the Fake implementation " 374 + "of Cronet Engine"); 375 } 376 synchronized (mLock) { 377 if (mIsShutdown) { 378 throw new IllegalStateException( 379 "This instance of CronetEngine has been shutdown and can no longer be " 380 + "used."); 381 } 382 throw new UnsupportedOperationException( 383 "The BidirectionalStream API is not supported by the Fake implementation of " 384 + "CronetEngine."); 385 } 386 } 387 388 /** 389 * Mark request as started to prevent shutdown when there are active 390 * requests, only if the engine is not shutdown. 391 * 392 * @return true if the engine is not shutdown and the request is marked as started. 393 */ startRequest()394 boolean startRequest() { 395 synchronized (mLock) { 396 if (!mIsShutdown) { 397 mActiveRequestCount++; 398 mRunningRequestCount++; 399 return true; 400 } 401 return false; 402 } 403 } 404 405 /** 406 * Mark request as destroyed to allow shutdown when there are no running 407 * requests. Should be called *before* the terminal callback is called, so 408 * that users can call shutdown() from the terminal callback. 409 */ onRequestDestroyed()410 void onRequestDestroyed() { 411 synchronized (mLock) { 412 // Verification check. We should not be able to shutdown if there are still running 413 // requests. 414 if (mIsShutdown) { 415 throw new IllegalStateException( 416 "This instance of CronetEngine was shutdown. All requests must have been " 417 + "complete."); 418 } 419 mRunningRequestCount--; 420 } 421 } 422 423 /** 424 * Mark request as finished for the purposes of getActiveRequestCount(). 425 * Should be called *after* the terminal callback returns. 426 */ onRequestFinished()427 void onRequestFinished() { 428 synchronized (mLock) { 429 mActiveRequestCount--; 430 } 431 } 432 } 433