content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * 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 Lice`nse 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 androidx.test.espresso.idling.net;
  
  import android.os.Handler;
  import android.os.Looper;
  import android.util.Log;
  import androidx.annotation.VisibleForTesting;
  import androidx.test.espresso.IdlingResource;
  import java.util.concurrent.CopyOnWriteArrayList;
  import java.util.concurrent.atomic.AtomicBoolean;
  import java.util.concurrent.atomic.AtomicInteger;
  import java.util.regex.Pattern;
  
  /**
   * An implementation of {@link IdlingResource} useful for monitoring idleness of network traffic.
   *
   * <p>This is similar to {@link androidx.test.espresso.contrib.CountingIdlingResource}, with
   * the additional idleness constraint that the counter must be 0 for a set period of time before the
   * resource becomes idle.
   *
   * <p>A network timeout is required to be reasonably sure that the webview has finished loading.
   * Imagine the case where each response that comes back causes another request to be made until
   * loading is complete. The counter will go from 0->1->0->1->0->1..., but we don't want to report
   * the webview as idle each time this happens.
   */
  public class UriIdlingResource implements IdlingResource {
  
    private static final String TAG = "UriIdlingResource";
    private final String resourceName;
    private final long timeoutMs;
    private final boolean debug;
  
    // Read and modified from multiple threads
    private final AtomicInteger counter = new AtomicInteger(0);
    private final CopyOnWriteArrayList<Pattern> ignoredRegexes = new CopyOnWriteArrayList<>();
    private final AtomicBoolean idle = new AtomicBoolean(true);
    private final Runnable transitionToIdle;
    private volatile ResourceCallback resourceCallback;
    private final HandlerIntf handler;
  
    public UriIdlingResource(String resourceName, long timeoutMs) {
      this(resourceName, timeoutMs, false, new DefaultHandler(new Handler(Looper.getMainLooper())));
    }
  
    @VisibleForTesting
    UriIdlingResource(String resourceName, long timeoutMs, boolean debug, HandlerIntf handler) {
      if (timeoutMs <= 0) {
        throw new IllegalArgumentException("timeoutMs has to be greater than 0");
      }
      this.resourceName = resourceName;
      this.timeoutMs = timeoutMs;
      this.debug = debug;
      this.handler = handler;
  
      transitionToIdle =
          new Runnable() {
            @Override
            public void run() {
              idle.set(true);
              if (resourceCallback != null) {
                resourceCallback.onTransitionToIdle();
              }
            }
          };
    }
  
    @Override
    public String getName() {
      return resourceName;
    }
  
    @Override
    public boolean isIdleNow() {
      return idle.get();
    }
  
    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
      this.resourceCallback = resourceCallback;
    }
  
    /**
     * Add a regex pattern to the ignore list.
     *
     * <p>All request URIs are checked against all patterns, and matches are ignored for the purposes
     * of detecting when the webview is idle.
     *
     * <p>Ignored patterns can only be added when the webview is idle.
     */
    public void ignoreUri(Pattern pattern) {
      if (!isIdleNow()) {
        Log.e(TAG, "Ignored patterns can only be added when the resource is idle.");
      } else {
        ignoredRegexes.add(pattern);
      }
    }
  
    /**
     * Called when a request is made.
     *
     * <p>If the URI is not blacklisted the idle counter is incremented.
     */
    public void beginLoad(String uri) {
      if (uriIsIgnored(uri)) {
        return;
      }
      idle.set(false);
      long count = counter.getAndIncrement();
      if (count == 0) {
        handler.removeCallbacks(transitionToIdle);
      }
      if (debug) {
        Log.i(TAG, "Resource " + resourceName + " counter increased to " + (count + 1));
      }
    }
  
    /**
     * Called when a request is completed (ie the response is returned).
     *
     * <p>If the URI is not blacklisted the idle counter is decremented. Once the idle counter reaches
     * 0, the idle update thread will set the resource as idle after the appropriate timeout.
     */
    public void endLoad(String uri) {
      if (uriIsIgnored(uri)) {
        return;
      }
      int count = counter.decrementAndGet();
      if (count < 0) {
        throw new IllegalStateException("Counter has been corrupted! Count=" + count);
      } else if (count == 0) {
        handler.postDelayed(transitionToIdle, timeoutMs);
      }
      if (debug) {
        Log.i(TAG, "Resource " + resourceName + " counter decreased to " + count);
      }
    }
  
    private boolean uriIsIgnored(String uri) {
      for (Pattern pattern : ignoredRegexes) {
        if (pattern.matcher(uri).matches()) {
          Log.i(TAG, "Resource " + resourceName + " ignored URI: <" + uri + ">");
          return true;
        }
      }
      return false;
    }
  
    @VisibleForTesting
    void forceIdleTransition() {
      transitionToIdle.run();
    }
  
    /**
     * Wraps a Handler object.
     *
     * <p>Mock this for testing purposes.
     */
    public static interface HandlerIntf {
      public void postDelayed(Runnable runnable, long millis);
  
      public void removeCallbacks(Runnable runnable);
    }
  
    private static final class DefaultHandler implements HandlerIntf {
      private final Handler handler;
  
      public DefaultHandler(Handler handler) {
        this.handler = handler;
      }
  
      @Override
      public void postDelayed(Runnable runnable, long millis) {
        handler.postDelayed(runnable, millis);
      }
  
      @Override
      public void removeCallbacks(Runnable runnable) {
        handler.removeCallbacks(runnable);
      }
    }
  }
  