aboutsummaryrefslogtreecommitdiff
path: root/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
blob: f624b60d5bb90ed083ba740df6a7f4ce827254be (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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static org.robolectric.RuntimeEnvironment.isMainThread;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;

import android.os.Looper;
import android.os.MessageQueue;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import org.robolectric.RoboSettings;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.config.ConfigurationRegistry;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.Scheduler;

/**
 * The shadow Looper implementation for {@link LooperMode.Mode.LEGACY}.
 *
 * <p>Robolectric enqueues posted {@link Runnable}s to be run (on this thread) later. {@code
 * Runnable}s that are scheduled to run immediately can be triggered by calling {@link #idle()}.
 *
 * @see ShadowMessageQueue
 */
@Implements(value = Looper.class, isInAndroidSdk = false)
@SuppressWarnings("SynchronizeOnNonFinalField")
public class ShadowLegacyLooper extends ShadowLooper {

  // Replaced SoftThreadLocal with a WeakHashMap, because ThreadLocal make it impossible to access
  // their contents from other threads, but we need to be able to access the loopers for all
  // threads so that we can shut them down when resetThreadLoopers()
  // is called. This also allows us to implement the useful getLooperForThread() method.
  // Note that the main looper is handled differently and is not put in this hash, because we need
  // to be able to "switch" the thread that the main looper is associated with.
  private static Map<Thread, Looper> loopingLoopers =
      Collections.synchronizedMap(new WeakHashMap<Thread, Looper>());

  private static Looper mainLooper;

  private static Scheduler backgroundScheduler;

  private @RealObject Looper realObject;

  boolean quit;

  @Resetter
  public static synchronized void resetThreadLoopers() {
    // do not use looperMode() here, because its cached value might already have been reset
    if (ConfigurationRegistry.get(LooperMode.Mode.class) == LooperMode.Mode.PAUSED) {
      // ignore if realistic looper
      return;
    }
    synchronized (loopingLoopers) {
      for (Looper looper : loopingLoopers.values()) {
        synchronized (looper) {
          if (!shadowOf(looper).quit) {
            looper.quit();
          } else {
            // Reset the schedulers of all loopers. This prevents un-run tasks queued up in static
            // background handlers from leaking to subsequent tests.
            shadowOf(looper).getScheduler().reset();
            shadowOf(looper.getQueue()).reset();
          }
        }
      }
    }
    // Because resetStaticState() is called by AndroidTestEnvironment on startup before
    // prepareMainLooper() is called, this might be null on that occasion.
    if (mainLooper != null) {
      shadowOf(mainLooper).reset();
    }
  }

  static synchronized Scheduler getBackgroundThreadScheduler() {
    return backgroundScheduler;
  }

  /** Internal API to initialize background thread scheduler from AndroidTestEnvironment. */
  public static void internalInitializeBackgroundThreadScheduler() {
    backgroundScheduler =
        RoboSettings.isUseGlobalScheduler()
            ? RuntimeEnvironment.getMasterScheduler()
            : new Scheduler();
  }

  @Implementation
  protected void __constructor__(boolean quitAllowed) {
    invokeConstructor(Looper.class, realObject, from(boolean.class, quitAllowed));
    if (isMainThread()) {
      mainLooper = realObject;
    } else {
      loopingLoopers.put(Thread.currentThread(), realObject);
    }
    resetScheduler();
  }

  @Implementation
  protected static Looper getMainLooper() {
    return mainLooper;
  }

  @Implementation
  protected static Looper myLooper() {
    return getLooperForThread(Thread.currentThread());
  }

  @Implementation
  protected static void loop() {
    shadowOf(Looper.myLooper()).doLoop();
  }

  private void doLoop() {
    if (realObject != Looper.getMainLooper()) {
      synchronized (realObject) {
        while (!quit) {
          try {
            realObject.wait();
          } catch (InterruptedException ignore) {
          }
        }
      }
    }
  }

  @Implementation
  protected void quit() {
    if (realObject == Looper.getMainLooper()) {
      throw new RuntimeException("Main thread not allowed to quit");
    }
    quitUnchecked();
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected void quitSafely() {
    quit();
  }

  @Override
  public void quitUnchecked() {
    synchronized (realObject) {
      quit = true;
      realObject.notifyAll();
      getScheduler().reset();
      shadowOf(realObject.getQueue()).reset();
    }
  }

  @Override
  public boolean hasQuit() {
    synchronized (realObject) {
      return quit;
    }
  }

  public static Looper getLooperForThread(Thread thread) {
    return isMainThread(thread) ? mainLooper : loopingLoopers.get(thread);
  }

  /** Return loopers for all threads including main thread. */
  protected static Collection<Looper> getLoopers() {
    List<Looper> loopers = new ArrayList<>(loopingLoopers.values());
    loopers.add(mainLooper);
    return Collections.unmodifiableCollection(loopers);
  }

  @Override
  public void idle() {
    idle(0, TimeUnit.MILLISECONDS);
  }

  @Override
  public void idleFor(long time, TimeUnit timeUnit) {
    getScheduler().advanceBy(time, timeUnit);
  }

  @Override
  public boolean isIdle() {
    return !getScheduler().areAnyRunnable();
  }

  @Override
  public void idleIfPaused() {
    // ignore
  }

  @Override
  public void idleConstantly(boolean shouldIdleConstantly) {
    getScheduler().idleConstantly(shouldIdleConstantly);
  }

  @Override
  public void runToEndOfTasks() {
    getScheduler().advanceToLastPostedRunnable();
  }

  @Override
  public void runToNextTask() {
    getScheduler().advanceToNextPostedRunnable();
  }

  @Override
  public void runOneTask() {
    getScheduler().runOneTask();
  }

  /**
   * Enqueue a task to be run later.
   *
   * @param runnable the task to be run
   * @param delayMillis how many milliseconds into the (virtual) future to run it
   * @return true if the runnable is enqueued
   * @see android.os.Handler#postDelayed(Runnable,long)
   * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
   */
  @Override
  @Deprecated
  public boolean post(Runnable runnable, long delayMillis) {
    if (!quit) {
      getScheduler().postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS);
      return true;
    } else {
      return false;
    }
  }

  /**
   * Enqueue a task to be run ahead of all other delayed tasks.
   *
   * @param runnable the task to be run
   * @return true if the runnable is enqueued
   * @see android.os.Handler#postAtFrontOfQueue(Runnable)
   * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
   */
  @Override
  @Deprecated
  public boolean postAtFrontOfQueue(Runnable runnable) {
    if (!quit) {
      getScheduler().postAtFrontOfQueue(runnable);
      return true;
    } else {
      return false;
    }
  }

  @Override
  public void pause() {
    getScheduler().pause();
  }

  @Override
  public Duration getNextScheduledTaskTime() {
    return getScheduler().getNextScheduledTaskTime();
  }

  @Override
  public Duration getLastScheduledTaskTime() {
    return getScheduler().getLastScheduledTaskTime();
  }

  @Override
  public void unPause() {
    getScheduler().unPause();
  }

  @Override
  public boolean isPaused() {
    return getScheduler().isPaused();
  }

  @Override
  public boolean setPaused(boolean shouldPause) {
    boolean wasPaused = isPaused();
    if (shouldPause) {
      pause();
    } else {
      unPause();
    }
    return wasPaused;
  }

  @Override
  public void resetScheduler() {
    ShadowMessageQueue shadowMessageQueue = shadowOf(realObject.getQueue());
    if (realObject == Looper.getMainLooper() || RoboSettings.isUseGlobalScheduler()) {
      shadowMessageQueue.setScheduler(RuntimeEnvironment.getMasterScheduler());
    } else {
      shadowMessageQueue.setScheduler(new Scheduler());
    }
  }

  /** Causes all enqueued tasks to be discarded, and pause state to be reset */
  @Override
  public void reset() {
    shadowOf(realObject.getQueue()).reset();
    resetScheduler();

    quit = false;
  }

  /**
   * Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued
   * tasks. This scheduler is managed by the Looper's associated queue.
   *
   * @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued
   *     tasks.
   */
  @Override
  public Scheduler getScheduler() {
    return shadowOf(realObject.getQueue()).getScheduler();
  }

  @Override
  public void runPaused(Runnable r) {
    boolean wasPaused = setPaused(true);
    try {
      r.run();
    } finally {
      if (!wasPaused) unPause();
    }
  }

  private static ShadowLegacyLooper shadowOf(Looper looper) {
    return Shadow.extract(looper);
  }

  private static ShadowMessageQueue shadowOf(MessageQueue mq) {
    return Shadow.extract(mq);
  }
}