content: /*
   * Copyright (C) 2015 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 License 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.rule;
  
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.os.IBinder;
  import android.util.Log;
  import androidx.annotation.NonNull;
  import androidx.annotation.VisibleForTesting;
  import androidx.test.internal.util.Checks;
  import androidx.test.platform.app.InstrumentationRegistry;
  import java.util.concurrent.CountDownLatch;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.TimeoutException;
  import org.junit.rules.TestRule;
  import org.junit.runner.Description;
  import org.junit.runners.model.Statement;
  
  /**
   * A JUnit rule that provides a simplified mechanism to start and shutdown your service before and
   * after the duration of your test. It also guarantees that the service is successfully connected
   * when starting (or binding to) a service. The service can be started (or bound) using one of the
   * helper methods. It will automatically be stopped (or unbound) after the test completes and any
   * methods annotated with <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>
   * After</code></a> are finished.
   *
   * <p>Note: This rule doesn't support {@link android.app.IntentService} because it's automatically
   * destroyed when {@link android.app.IntentService#onHandleIntent(android.content.Intent)} finishes
   * all outstanding commands. So there is no guarantee to establish a successful connection in a
   * timely manner.
   *
   * <p>Usage:
   *
   * <pre>
   * &#064;Rule
   * public final ServiceTestRule mServiceRule = new ServiceTestRule();
   *
   * &#064;Test
   * public void testWithStartedService() {
   *     mServiceRule.startService(
   *         new Intent(InstrumentationRegistry.getTargetContext(), MyService.class));
   *     //do something
   * }
   *
   * &#064;Test
   * public void testWithBoundService() {
   *     IBinder binder = mServiceRule.bindService(
   *         new Intent(InstrumentationRegistry.getTargetContext(), MyService.class));
   *     MyService service = ((MyService.LocalBinder) binder).getService();
   *     assertTrue("True wasn't returned", service.doSomethingToReturnTrue());
   * }
   * </pre>
   */
  public class ServiceTestRule implements TestRule {
  
    private static final String TAG = "ServiceTestRule";
    private static final long DEFAULT_TIMEOUT = 5L; // seconds
  
    private IBinder binder;
    private Intent serviceIntent;
    private ServiceConnection serviceConn;
    private final long timeout;
    private final TimeUnit timeUnit;
  
    boolean serviceStarted = false;
    boolean serviceBound = false;
  
    /** Creates a {@link ServiceTestRule} with a default timeout of 5 seconds */
    public ServiceTestRule() {
      this(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
    }
  
    protected ServiceTestRule(long timeout, TimeUnit timeUnit) {
      this.timeout = timeout;
      this.timeUnit = timeUnit;
    }
  
    /**
     * Factory method to create a {@link ServiceTestRule} with a custom timeout
     *
     * @param timeout the amount of time to wait for a service to connect.
     * @param timeUnit the time unit representing how the timeout parameter should be interpreted
     * @return a {@link ServiceTestRule} with the desired timeout
     */
    public static ServiceTestRule withTimeout(long timeout, TimeUnit timeUnit) {
      return new ServiceTestRule(timeout, timeUnit);
    }
  
    /**
     * Starts the service under test and blocks until the service is connected, in the same way as if
     * it were started by {@link android.content.Context#startService(Intent)
     * Context.startService(Intent)} with an {@link android.content.Intent} that identifies a service.
     * If you use this method to start the service, it is automatically stopped at the end of the test
     * run. However, it also attempts to bind to the service and waits for {@link
     * ServiceConnection#onServiceConnected(android.content.ComponentName, android.os.IBinder)} to be
     * called to ensure successful connection.
     *
     * <p><b>Note:</b> This method only supports services that allow clients to bind to them. In other
     * words, if your service {@link android.app.Service#onBind(Intent)} method returns {@code null}
     * then a {@link TimeoutException} will be thrown.
     *
     * @param intent An Intent that identifies a service, of the same form as the Intent passed to
     *     {@link android.content.Context#startService(Intent) Context.startService (Intent)}.
     * @throws SecurityException if you do not have permission to bind to the given service.
     * @throws TimeoutException if timed out waiting for a successful connection with the service.
     * @throws IllegalArgumentException if the intent did not resolve to a service
     */
    public void startService(@NonNull Intent intent) throws TimeoutException {
      serviceIntent = Checks.checkNotNull(intent, "intent can't be null");
      ComponentName componentName =
          InstrumentationRegistry.getInstrumentation().getTargetContext().startService(serviceIntent);
      if (componentName == null) {
        throw new IllegalArgumentException(
            "Intent "
                + intent
                + "did not start a service - this could be triggered by:\n"
                + "  1. Your service is not declared in the manifest\n"
                + "  2. The service is not visible to your app, because of package filtering\n");
      }
      serviceStarted = true;
  
      // bind to the started service to guarantee its started and connected before test execution
      serviceBound = bindServiceAndWait(serviceIntent, null, Context.BIND_AUTO_CREATE);
    }
  
    /**
     * Works just like {@link #bindService(android.content.Intent, android.content.ServiceConnection,
     * int)} except uses an internal {@link android.content.ServiceConnection} to guarantee successful
     * bound. The operation option flag defaults to {@link android.content.Context#BIND_AUTO_CREATE}
     *
     * @see #bindService(android.content.Intent, android.content.ServiceConnection, int)
     */
    public IBinder bindService(@NonNull Intent intent) throws TimeoutException {
      // no extras are expected by unbind
      serviceIntent = Checks.checkNotNull(intent, "intent can't be null").cloneFilter();
      serviceBound = bindServiceAndWait(intent, null, Context.BIND_AUTO_CREATE);
      return binder;
    }
  
    /**
     * Binds the service under test, in the same way as if it were started by {@link
     * android.content.Context#bindService(Intent, ServiceConnection, int) Context.bindService(Intent,
     * ServiceConnection, flags)} with an {@link android.content.Intent} that identifies a service.
     * However, it waits for {@link
     * ServiceConnection#onServiceConnected(android.content.ComponentName, android.os.IBinder)} to be
     * called before returning.
     *
     * @param intent Identifies the service to connect to. The Intent may specify either an explicit
     *     component name, or a logical description (action, category, etc) to match an {@link
     *     android.content.IntentFilter} published by a service.
     * @param connection Receives information as the service is started and stopped. This must be a
     *     valid ServiceConnection object; it must not be null.
     * @param flags Operation options for the binding. May be 0, {@link
     *     android.content.Context#BIND_AUTO_CREATE}, {@link
     *     android.content.Context#BIND_DEBUG_UNBIND}, {@link
     *     android.content.Context#BIND_NOT_FOREGROUND}, {@link
     *     android.content.Context#BIND_ABOVE_CLIENT}, {@link
     *     android.content.Context#BIND_ALLOW_OOM_MANAGEMENT}, or {@link
     *     android.content.Context#BIND_WAIVE_PRIORITY}.
     * @return An object whose type is a subclass of IBinder, for making further calls into the
     *     service.
     * @throws SecurityException if the called doesn't have permission to bind to the given service.
     * @throws TimeoutException if timed out waiting for a successful connection with the service.
     * @see android.content.Context#BIND_AUTO_CREATE
     * @see android.content.Context#BIND_DEBUG_UNBIND
     * @see android.content.Context#BIND_NOT_FOREGROUND
     */
    public IBinder bindService(
        @NonNull Intent intent, @NonNull ServiceConnection connection, int flags)
        throws TimeoutException {
      // no extras are expected by unbind
      serviceIntent = Checks.checkNotNull(intent, "intent can't be null").cloneFilter();
      ServiceConnection c = Checks.checkNotNull(connection, "connection can't be null");
      serviceBound = bindServiceAndWait(serviceIntent, c, flags);
  
      return binder;
    }
  
    @VisibleForTesting
    boolean bindServiceAndWait(Intent intent, final ServiceConnection conn, int flags)
        throws TimeoutException {
  
      ProxyServiceConnection serviceConn = new ProxyServiceConnection(conn);
  
      boolean isBound =
          InstrumentationRegistry.getInstrumentation()
              .getTargetContext()
              .bindService(intent, serviceConn, flags);
  
      if (isBound) {
        // block until service connection is established
        waitOnLatch(serviceConn.connectedLatch, "connected");
        this.serviceConn = serviceConn;
      } else {
        Log.e(TAG, "Failed to bind to service! Is your service declared in the manifest?");
      }
  
      return isBound;
    }
  
    /**
     * Unbinds the service under test that was previously bound by a call to {@link
     * #bindService(android.content.Intent)} or {@link #bindService(android.content.Intent,
     * android.content.ServiceConnection, int)}. You normally do not need to call this method since
     * your service will automatically be stopped and unbound at the end of each test method.
     */
    public void unbindService() {
      if (serviceBound) {
        InstrumentationRegistry.getInstrumentation().getTargetContext().unbindService(serviceConn);
        binder = null;
        serviceBound = false;
      }
    }
  
    /**
     * This class is used to wait until a successful connection to the service was established. It
     * then serves as a proxy to original {@link android.content.ServiceConnection} passed by the
     * caller.
     */
    class ProxyServiceConnection implements ServiceConnection {
      private ServiceConnection callerConnection;
      public CountDownLatch connectedLatch = new CountDownLatch(1);
  
      private ProxyServiceConnection(ServiceConnection connection) {
        callerConnection = connection;
      }
  
      @Override
      public void onServiceConnected(ComponentName name, IBinder service) {
        // store the service binder to return to the caller
        binder = service;
        if (callerConnection != null) {
          // pass through everything to the callers ServiceConnection
          callerConnection.onServiceConnected(name, service);
        }
        connectedLatch.countDown();
      }
  
      @Override
      public void onServiceDisconnected(ComponentName name) {
        // The process hosting the service has crashed or been killed.
        Log.e(TAG, "Connection to the Service has been lost!");
        binder = null;
        if (callerConnection != null) {
          // pass through everything to the callers ServiceConnection
          callerConnection.onServiceDisconnected(name);
        }
      }
    }
  
    /** Helper method to block on a given latch for the duration of the set timeout */
    private void waitOnLatch(CountDownLatch latch, String actionName) throws TimeoutException {
      try {
        if (!latch.await(timeout, timeUnit)) {
          throw new TimeoutException(
              "Waited for "
                  + timeout
                  + " "
                  + timeUnit.name()
                  + ","
                  + " but service was never "
                  + actionName);
        }
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("Interrupted while waiting for service to be " + actionName, e);
      }
    }
  
    /**
     * Makes the necessary calls to stop (or unbind) the service under test. This method is called
     * automatically called after test execution. This is not a blocking call since there is no
     * reliable way to guarantee successful disconnect without access to service lifecycle.
     */
    @VisibleForTesting
    void shutdownService() throws TimeoutException {
      if (serviceStarted) {
        InstrumentationRegistry.getInstrumentation().getTargetContext().stopService(serviceIntent);
        serviceStarted = false;
      }
      unbindService();
    }
  
    /**
     * Override this method to do your own service specific initialization before starting or binding
     * to the service. The method is called before each test method is executed including any method
     * annotated with <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>
     * Before</code></a>. Do not start or bind to a service from here!
     */
    protected void beforeService() {
      // empty by default
    }
  
    /**
     * Override this method to do your own service specific clean up after the service is shutdown.
     * The method is called after each test method is executed including any method annotated with <a
     * href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a> and
     * after necessary calls to stop (or unbind) the service under test were called.
     */
    protected void afterService() {
      // empty by default
    }
  
    @Override
    public Statement apply(final Statement base, Description description) {
      return new ServiceStatement(base);
    }
  
    /**
     * {@link Statement} that executes the service lifecycle methods before and after the execution of
     * the test.
     */
    private class ServiceStatement extends Statement {
      private final Statement base;
  
      public ServiceStatement(Statement base) {
        this.base = base;
      }
  
      @Override
      public void evaluate() throws Throwable {
        try {
          beforeService();
          base.evaluate();
        } finally {
          shutdownService();
          afterService();
        }
      }
    }
  }
  