summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/UiThreadUtils.java
blob: f5aee4db3b344c32aba2ecc2bbd90186a77f9676 (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
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

package org.mozilla.geckoview.test.util;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import androidx.annotation.NonNull;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import org.mozilla.geckoview.GeckoResult;

public class UiThreadUtils {
  private static Method sGetNextMessage = null;

  static {
    try {
      sGetNextMessage = MessageQueue.class.getDeclaredMethod("next");
      sGetNextMessage.setAccessible(true);
    } catch (final NoSuchMethodException e) {
      throw new IllegalStateException(e);
    }
  }

  public static class TimeoutException extends RuntimeException {
    public TimeoutException(final String detailMessage) {
      super(detailMessage);
    }
  }

  private static final class TimeoutRunnable implements Runnable {
    private long timeout;

    public void set(final long timeout) {
      this.timeout = timeout;
      cancel();
      HANDLER.postDelayed(this, timeout);
    }

    public void cancel() {
      HANDLER.removeCallbacks(this);
    }

    @Override
    public void run() {
      throw new TimeoutException("Timed out after " + timeout + "ms");
    }
  }

  public static final Handler HANDLER = new Handler(Looper.getMainLooper());
  private static final TimeoutRunnable TIMEOUT_RUNNABLE = new TimeoutRunnable();

  private static RuntimeException unwrapRuntimeException(final Throwable e) {
    final Throwable cause = e.getCause();
    if (cause != null && cause instanceof RuntimeException) {
      return (RuntimeException) cause;
    } else if (e instanceof RuntimeException) {
      return (RuntimeException) e;
    }

    return new RuntimeException(cause != null ? cause : e);
  }

  /**
   * This waits for the given result and returns it's value. If the result failed with an exception,
   * it is rethrown.
   *
   * @param result A {@link GeckoResult} instance.
   * @param <T> The type of the value held by the {@link GeckoResult}
   * @return The value of the completed {@link GeckoResult}.
   */
  public static <T> T waitForResult(@NonNull final GeckoResult<T> result, final long timeout)
      throws Throwable {
    final ResultHolder<T> holder = new ResultHolder<>(result);

    waitForCondition(() -> holder.isComplete, timeout);

    if (holder.error != null) {
      throw holder.error;
    }

    return holder.value;
  }

  private static class ResultHolder<T> {
    public T value;
    public Throwable error;
    public boolean isComplete;

    public ResultHolder(final GeckoResult<T> result) {
      result.accept(
          value -> {
            ResultHolder.this.value = value;
            isComplete = true;
          },
          error -> {
            ResultHolder.this.error = error;
            isComplete = true;
          });
    }
  }

  public interface Condition {
    boolean test();
  }

  public static void loopUntilIdle(final long timeout) {
    final AtomicBoolean idle = new AtomicBoolean(false);

    MessageQueue.IdleHandler handler = null;
    try {
      handler =
          () -> {
            idle.set(true);
            // Remove handler
            return false;
          };

      HANDLER.getLooper().getQueue().addIdleHandler(handler);

      waitForCondition(() -> idle.get(), timeout);
    } finally {
      if (handler != null) {
        HANDLER.getLooper().getQueue().removeIdleHandler(handler);
      }
    }
  }

  public static void waitForCondition(final Condition condition, final long timeout) {
    // Adapted from GeckoThread.pumpMessageLoop.
    final MessageQueue queue = HANDLER.getLooper().getQueue();

    TIMEOUT_RUNNABLE.set(timeout);

    MessageQueue.IdleHandler handler = null;
    try {
      handler =
          () -> {
            HANDLER.postDelayed(() -> {}, 100);
            return true;
          };

      HANDLER.getLooper().getQueue().addIdleHandler(handler);
      while (!condition.test()) {
        final Message msg;
        try {
          msg = (Message) sGetNextMessage.invoke(queue);
        } catch (final IllegalAccessException | InvocationTargetException e) {
          throw unwrapRuntimeException(e);
        }
        if (msg.getTarget() == null) {
          HANDLER.getLooper().quit();
          return;
        }
        msg.getTarget().dispatchMessage(msg);
      }
    } finally {
      TIMEOUT_RUNNABLE.cancel();
      if (handler != null) {
        HANDLER.getLooper().getQueue().removeIdleHandler(handler);
      }
    }
  }
}