aboutsummaryrefslogtreecommitdiff
path: root/shadows/framework/src/main/java/org/robolectric/android/util/concurrent/PausedExecutorService.java
blob: cd16c11d93bad6da9cc0c287c968bb67a8bb7f37 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package org.robolectric.android.util.concurrent;

import androidx.annotation.NonNull;
import com.google.common.annotations.Beta;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.AbstractFuture;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.TimeUnit;
import org.robolectric.annotation.LooperMode;
import org.robolectric.util.Logger;

/**
 * Executor service that queues any posted tasks.
 *
 * Users must explicitly call {@link runAll()} to execute all pending tasks.
 *
 * Intended to be a replacement for {@link RoboExecutorService} when using
 * {@link LooperMode.Mode#PAUSED}.
 * Unlike {@link RoboExecutorService}, will execute tasks on a background thread. This is useful
 * to test Android code that enforces it runs off the main thread.
 *
 * NOTE: Beta API, subject to change.
 */
@Beta
public class PausedExecutorService extends AbstractExecutorService {

  /**
   * Run given callable on the given executor and try to preserve original exception if possible.
   */
  static <T> T getFutureResultWithExceptionPreserved(Future<T> future) {
    try {
      return future.get();
    } catch (ExecutionException e) {
      // try to preserve original exception if possible
      Throwable cause = e.getCause();
      if (cause == null) {
        throw new RuntimeException(e);
      } else if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      } else {
        throw new RuntimeException(cause);
      }
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  private final ExecutorService realService;
  private final Queue<Runnable> deferredTasks = new ConcurrentLinkedQueue<>();
  private Thread executorThread;

  private static class DeferredTask<V> extends AbstractFuture<V> implements RunnableFuture<V> {

    private final Callable<V> callable;
    private final ExecutorService executor;

    DeferredTask(Callable<V> callable, ExecutorService executor) {
      this.callable = callable;
      this.executor = executor;
    }

    @Override
    public void run() {
      Future<V> future = executor.submit(callable);
      set(getFutureResultWithExceptionPreserved(future));
    }
  }

  public PausedExecutorService() {
    this.realService =
        Executors.newSingleThreadExecutor(
            r -> {
              executorThread = new Thread(r);
              return executorThread;
            });
  }

  /**
   * Execute all posted tasks and block until they are complete.
   *
   * @return the number of tasks executed
   */
  public int runAll() {
    int numTasksRun = 0;
    if (Thread.currentThread().equals(executorThread)) {
      Logger.info("ignoring request to execute task - called from executor's own thread");
      return numTasksRun;
    }
    while (hasQueuedTasks()) {
      runNext();
      numTasksRun++;
    }
    return numTasksRun;
  }

  /**
   * Executes the next queued task.
   *
   * Will be ignored if called from the executor service thread to prevent deadlocks.
   *
   * @return true if task was run, false if queue was empty
   */
  public boolean runNext() {
    if (!hasQueuedTasks()) {
      return false;
    }
    if (Thread.currentThread().equals(executorThread)) {
      Logger.info("ignoring request to execute task - called from executor's own thread");
      return false;
    }
    Runnable task = deferredTasks.poll();
    task.run();
    return true;
  }

  /**
   * @return true if there are queued pending tasks
   */
  public boolean hasQueuedTasks() {
    return !deferredTasks.isEmpty();
  }

  @Override
  public void shutdown() {
    realService.shutdown();
    deferredTasks.clear();
  }

  @Override
  public List<Runnable> shutdownNow() {
    realService.shutdownNow();
    List<Runnable> copy = ImmutableList.copyOf(deferredTasks);
    deferredTasks.clear();
    return copy;
  }

  @Override
  public boolean isShutdown() {
    return realService.isShutdown();
  }

  @Override
  public boolean isTerminated() {
    return realService.isTerminated();
  }

  @Override
  public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException {
    // If not shut down first, timeout would occur with normal behavior.
    return realService.awaitTermination(l, timeUnit);
  }

  @Override
  public void execute(@NonNull Runnable command) {
    if (command instanceof DeferredTask) {
      deferredTasks.add(command);
    } else {
      deferredTasks.add(new DeferredTask<>(Executors.callable(command), realService));
    }
  }

  @Override
  protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return newTaskFor(Executors.callable(runnable, value));
  }

  @Override
  protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new DeferredTask<T>(callable, realService);
  }
}