| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.net.test; |
| |
| import android.content.Context; |
| |
| import androidx.annotation.GuardedBy; |
| |
| import org.chromium.net.BidirectionalStream; |
| import org.chromium.net.CronetEngine; |
| import org.chromium.net.ExperimentalBidirectionalStream; |
| import org.chromium.net.NetworkQualityRttListener; |
| import org.chromium.net.NetworkQualityThroughputListener; |
| import org.chromium.net.RequestFinishedInfo; |
| import org.chromium.net.UrlRequest; |
| import org.chromium.net.impl.CronetEngineBase; |
| import org.chromium.net.impl.CronetEngineBuilderImpl; |
| import org.chromium.net.impl.ImplVersion; |
| import org.chromium.net.impl.RefCountDelegate; |
| import org.chromium.net.impl.UrlRequestBase; |
| import org.chromium.net.impl.VersionSafeCallbacks; |
| |
| import java.io.IOException; |
| import java.net.Proxy; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.net.URLStreamHandlerFactory; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.ThreadFactory; |
| import java.util.concurrent.ThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| |
| /** Fake {@link CronetEngine}. This implements CronetEngine. */ |
| final class FakeCronetEngine extends CronetEngineBase { |
| /** Builds a {@link FakeCronetEngine}. This implements CronetEngine.Builder. */ |
| static class Builder extends CronetEngineBuilderImpl { |
| private FakeCronetController mController; |
| |
| /** |
| * Builder for {@link FakeCronetEngine}. |
| * |
| * @param context Android {@link Context}. |
| */ |
| Builder(Context context) { |
| super(context); |
| } |
| |
| @Override |
| public FakeCronetEngine build() { |
| return new FakeCronetEngine(this); |
| } |
| |
| void setController(FakeCronetController controller) { |
| mController = controller; |
| } |
| } |
| |
| private final FakeCronetController mController; |
| private final ExecutorService mExecutorService; |
| |
| private final Object mLock = new Object(); |
| |
| @GuardedBy("mLock") |
| private boolean mIsShutdown; |
| |
| /** |
| * The number of started requests where the terminal callback (i.e. |
| * onSucceeded/onCancelled/onFailed) has not yet been called. |
| */ |
| @GuardedBy("mLock") |
| private int mRunningRequestCount; |
| |
| /* |
| * The number of started requests where the terminal callbacks (i.e. |
| * onSucceeded/onCancelled/onFailed, request finished listeners) have not |
| * all returned yet. |
| * |
| * By definition this is always greater than or equal to |
| * mRunningRequestCount. The difference between the two is the number of |
| * terminal callbacks that are currently running. |
| */ |
| @GuardedBy("mLock") |
| private int mActiveRequestCount; |
| |
| @GuardedBy("mLock") |
| private final Map< |
| RequestFinishedInfo.Listener, VersionSafeCallbacks.RequestFinishedInfoListener> |
| mFinishedListenerMap = new HashMap<>(); |
| |
| /** |
| * Creates a {@link FakeCronetEngine}. Used when {@link FakeCronetEngine} is created with the |
| * {@link FakeCronetEngine.Builder}. |
| * |
| * @param builder a {@link CronetEngineBuilderImpl} to build this {@link CronetEngine} |
| * implementation from. |
| */ |
| private FakeCronetEngine(FakeCronetEngine.Builder builder) { |
| if (builder.mController != null) { |
| mController = builder.mController; |
| } else { |
| mController = new FakeCronetController(); |
| } |
| mExecutorService = |
| new ThreadPoolExecutor( |
| /* corePoolSize= */ 1, |
| /* maximumPoolSize= */ 5, |
| /* keepAliveTime= */ 50, |
| TimeUnit.SECONDS, |
| new LinkedBlockingQueue<Runnable>(), |
| new ThreadFactory() { |
| @Override |
| public Thread newThread(final Runnable r) { |
| return Executors.defaultThreadFactory() |
| .newThread( |
| () -> { |
| Thread.currentThread() |
| .setName("FakeCronetEngine"); |
| r.run(); |
| }); |
| } |
| }); |
| FakeCronetController.addFakeCronetEngine(this); |
| } |
| |
| /** |
| * Gets the controller associated with this instance that will be used for responses to |
| * {@link UrlRequest}s. |
| * |
| * @return the {@link FakeCronetCntroller} that controls this {@link FakeCronetEngine}. |
| */ |
| FakeCronetController getController() { |
| return mController; |
| } |
| |
| @Override |
| public ExperimentalBidirectionalStream.Builder newBidirectionalStreamBuilder( |
| String url, BidirectionalStream.Callback callback, Executor executor) { |
| synchronized (mLock) { |
| if (mIsShutdown) { |
| throw new IllegalStateException( |
| "This instance of CronetEngine has been shutdown and can no longer be " |
| + "used."); |
| } |
| throw new UnsupportedOperationException( |
| "The bidirectional stream API is not supported by the Fake implementation " |
| + "of CronetEngine."); |
| } |
| } |
| |
| @Override |
| public String getVersionString() { |
| return "FakeCronet/" + ImplVersion.getCronetVersionWithLastChange(); |
| } |
| |
| @Override |
| public void shutdown() { |
| synchronized (mLock) { |
| if (mRunningRequestCount != 0) { |
| throw new IllegalStateException("Cannot shutdown with running requests."); |
| } else { |
| mIsShutdown = true; |
| } |
| } |
| mExecutorService.shutdown(); |
| FakeCronetController.removeFakeCronetEngine(this); |
| } |
| |
| @Override |
| public void startNetLogToFile(String fileName, boolean logAll) {} |
| |
| @Override |
| public void startNetLogToDisk(String dirPath, boolean logAll, int maxSize) {} |
| |
| @Override |
| public void stopNetLog() {} |
| |
| @Override |
| public byte[] getGlobalMetricsDeltas() { |
| return new byte[0]; |
| } |
| |
| @Override |
| public int getEffectiveConnectionType() { |
| return EFFECTIVE_CONNECTION_TYPE_UNKNOWN; |
| } |
| |
| @Override |
| public int getHttpRttMs() { |
| return CONNECTION_METRIC_UNKNOWN; |
| } |
| |
| @Override |
| public int getTransportRttMs() { |
| return CONNECTION_METRIC_UNKNOWN; |
| } |
| |
| @Override |
| public int getDownstreamThroughputKbps() { |
| return CONNECTION_METRIC_UNKNOWN; |
| } |
| |
| @Override |
| public void bindToNetwork(long networkHandle) { |
| throw new UnsupportedOperationException( |
| "The multi-network API is not supported by the Fake implementation " |
| + "of Cronet Engine"); |
| } |
| |
| @Override |
| public void configureNetworkQualityEstimatorForTesting( |
| boolean useLocalHostRequests, |
| boolean useSmallerResponses, |
| boolean disableOfflineCheck) {} |
| |
| @Override |
| public void addRttListener(NetworkQualityRttListener listener) {} |
| |
| @Override |
| public void removeRttListener(NetworkQualityRttListener listener) {} |
| |
| @Override |
| public void addThroughputListener(NetworkQualityThroughputListener listener) {} |
| |
| @Override |
| public void removeThroughputListener(NetworkQualityThroughputListener listener) {} |
| |
| @Override |
| public void addRequestFinishedListener(RequestFinishedInfo.Listener listener) { |
| if (listener == null) { |
| throw new IllegalArgumentException("Listener must not be null"); |
| } |
| synchronized (mLock) { |
| mFinishedListenerMap.put( |
| listener, new VersionSafeCallbacks.RequestFinishedInfoListener(listener)); |
| } |
| } |
| |
| @Override |
| public void removeRequestFinishedListener(RequestFinishedInfo.Listener listener) { |
| if (listener == null) { |
| throw new IllegalArgumentException("Listener must not be null"); |
| } |
| synchronized (mLock) { |
| mFinishedListenerMap.remove(listener); |
| } |
| } |
| |
| boolean hasRequestFinishedListeners() { |
| synchronized (mLock) { |
| return !mFinishedListenerMap.isEmpty(); |
| } |
| } |
| |
| void reportRequestFinished( |
| RequestFinishedInfo requestInfo, RefCountDelegate inflightDoneCallbackCount) { |
| synchronized (mLock) { |
| for (RequestFinishedInfo.Listener listener : mFinishedListenerMap.values()) { |
| inflightDoneCallbackCount.increment(); |
| listener.getExecutor() |
| .execute( |
| () -> { |
| try { |
| listener.onRequestFinished(requestInfo); |
| } finally { |
| inflightDoneCallbackCount.decrement(); |
| } |
| }); |
| } |
| } |
| } |
| |
| // TODO(crbug.com/669707) Instantiate a fake CronetHttpUrlConnection wrapping a FakeUrlRequest |
| // here. |
| @Override |
| public URLConnection openConnection(URL url) throws IOException { |
| throw new UnsupportedOperationException( |
| "The openConnection API is not supported by the Fake implementation of " |
| + "CronetEngine."); |
| } |
| |
| @Override |
| public URLConnection openConnection(URL url, Proxy proxy) throws IOException { |
| throw new UnsupportedOperationException( |
| "The openConnection API is not supported by the Fake implementation of " |
| + "CronetEngine."); |
| } |
| |
| @Override |
| public URLStreamHandlerFactory createURLStreamHandlerFactory() { |
| throw new UnsupportedOperationException( |
| "The URLStreamHandlerFactory API is not supported by the Fake implementation of " |
| + "CronetEngine."); |
| } |
| |
| @Override |
| protected UrlRequestBase 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) { |
| if (networkHandle != DEFAULT_NETWORK_HANDLE) { |
| throw new UnsupportedOperationException( |
| "The multi-network API is not supported by the Fake implementation " |
| + "of Cronet Engine"); |
| } |
| |
| synchronized (mLock) { |
| if (mIsShutdown) { |
| throw new IllegalStateException( |
| "This instance of CronetEngine has been shutdown and can no longer be " |
| + "used."); |
| } |
| return new FakeUrlRequest( |
| callback, |
| userExecutor, |
| mExecutorService, |
| url, |
| allowDirectExecutor, |
| trafficStatsTagSet, |
| trafficStatsTag, |
| trafficStatsUidSet, |
| trafficStatsUid, |
| mController, |
| this, |
| connectionAnnotations); |
| } |
| } |
| |
| @Override |
| public int getActiveRequestCount() { |
| synchronized (mLock) { |
| return mActiveRequestCount; |
| } |
| } |
| |
| @Override |
| protected ExperimentalBidirectionalStream 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) { |
| if (networkHandle != DEFAULT_NETWORK_HANDLE) { |
| throw new UnsupportedOperationException( |
| "The multi-network API is not supported by the Fake implementation " |
| + "of Cronet Engine"); |
| } |
| synchronized (mLock) { |
| if (mIsShutdown) { |
| throw new IllegalStateException( |
| "This instance of CronetEngine has been shutdown and can no longer be " |
| + "used."); |
| } |
| throw new UnsupportedOperationException( |
| "The BidirectionalStream API is not supported by the Fake implementation of " |
| + "CronetEngine."); |
| } |
| } |
| |
| /** |
| * Mark request as started to prevent shutdown when there are active |
| * requests, only if the engine is not shutdown. |
| * |
| * @return true if the engine is not shutdown and the request is marked as started. |
| */ |
| boolean startRequest() { |
| synchronized (mLock) { |
| if (!mIsShutdown) { |
| mActiveRequestCount++; |
| mRunningRequestCount++; |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Mark request as destroyed to allow shutdown when there are no running |
| * requests. Should be called *before* the terminal callback is called, so |
| * that users can call shutdown() from the terminal callback. |
| */ |
| void onRequestDestroyed() { |
| synchronized (mLock) { |
| // Verification check. We should not be able to shutdown if there are still running |
| // requests. |
| if (mIsShutdown) { |
| throw new IllegalStateException( |
| "This instance of CronetEngine was shutdown. All requests must have been " |
| + "complete."); |
| } |
| mRunningRequestCount--; |
| } |
| } |
| |
| /** |
| * Mark request as finished for the purposes of getActiveRequestCount(). |
| * Should be called *after* the terminal callback returns. |
| */ |
| void onRequestFinished() { |
| synchronized (mLock) { |
| mActiveRequestCount--; |
| } |
| } |
| } |