aboutsummaryrefslogtreecommitdiffstats
path: root/tests/src/org/cyanogenmod/tests/common/ThreadServiceTestCase.java
blob: 4404377de5f6302d9e65beaf6150d0854a2be216 (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
package org.cyanogenmod.tests.common;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import android.test.ServiceTestCase;

/**
 * Tests a service in its own Thread.
 *
 *
 * <p>
 * The {@link ServiceTestCase} class creates the service in the same thread the
 * test is running. In consequence Handlers and other constructs that depend on
 * the fact that the service methods are always run on the <em>main thread</em>
 * won't work.
 * </p>
 *
 * <p>
 * To circumvent this, this test class creates a {@link HandlerThread} on setup
 * to simulate the main tread and provides helper constructs to ease the
 * communication between the Service and the test class :
 * </p>
 *
 * <ul>
 * <li>The {@link #runOnServiceThread(Runnable)} methods allows to run code on
 * the service pseudo-main thread.</li>
 * <li>The {@link #startService(boolean, ServiceRunnable)} mehod allows starting
 * the service in its own thread with some additional initialization code.</li>
 * </ul>
 *
 *
 * @author Antoine Martin
 *
 */
public abstract class ThreadServiceTestCase<T extends Service> extends ServiceTestCase<T> {

    /** Typical maximum wait time for something to happen on the service */
    public static final long WAIT_TIME = 5 * 1000;

    /*
     * This class provides final mutable values through indirection
     */
    static class Holder<H> {
        H value;
    }

    protected Handler serviceHandler;
    protected Looper serviceLooper;
    /*
     * Got to catch this again because of damn package visibility of
     * mServiceClass in base class.
     */
    protected Class<T> serviceClass;

    public ThreadServiceTestCase(Class<T> serviceClass) {
        super(serviceClass);
        this.serviceClass = serviceClass;
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        // Setup service thread
        HandlerThread serviceThread = new HandlerThread("[" + serviceClass.getSimpleName() + "Thread]");
        serviceThread.start();
        serviceLooper = serviceThread.getLooper();
        serviceHandler = new Handler(serviceLooper);
    }

    @Override
    public void testServiceTestCaseSetUpProperly() throws Exception {
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        // teardown service thread
        if (serviceLooper != null)
            serviceLooper.quit();
        serviceHandler = null;
    }

    /**
     * Runs the specified runnable on the service tread and waits for its
     * completion.
     *
     * @see InstrumentationTestCase#runTestOnUiThread(Runnable)
     * @param r
     *            The runnable to run on the pseudo-main thread.
     */
    protected void runOnServiceThread(final Runnable r) {
        final CountDownLatch serviceSignal = new CountDownLatch(1);
        serviceHandler.post(new Runnable() {

            @Override
            public void run() {
                r.run();
                serviceSignal.countDown();
            }
        });

        try {
            serviceSignal.await();
        } catch (InterruptedException ie) {
            fail("The Service thread has been interrupted");
        }
    }

    /**
     * Runnable interface allowing service initialization personalization.
     *
     * @author Antoine Martin
     *
     */
    protected interface ServiceRunnable {
        public void run(Service service);
    }

    /**
     * Initialize the service in its own thread and returns it.
     *
     * @param bound
     *            if {@code true}, the service will be created as if it was
     *            bound by a client. if {@code false}, it will be created by a
     *            {@code startService} call.
     * @param r
     *            {@link ServiceRunnable} instance that will be called with the
     *            created service.
     * @return The created service.
     */
    protected T startService(final ServiceRunnable r) {
        final Holder<T> serviceHolder = new Holder<T>();

        // I want to create my service in its own 'Main thread'
        // So it can use its handler
        runOnServiceThread(new Runnable() {

            @Override
            public void run() {
                T service = null;
                startService(new Intent(getContext(), serviceClass));
                service = getService();
                if (r != null)
                    r.run(service);
                serviceHolder.value = service;
            }
        });

        return serviceHolder.value;
    }

    protected IBinder bindService(final ServiceRunnable r) {
        final Holder<IBinder> serviceHolder = new Holder<IBinder>();

        // I want to create my service in its own 'Main thread'
        // So it can use its handler
        runOnServiceThread(new Runnable() {

            @Override
            public void run() {
                T service = null;
                IBinder binder = bindService(new Intent(getContext(), serviceClass));
                service = getService();
                if (r != null)
                    r.run(service);
                serviceHolder.value = binder;
            }
        });

        return serviceHolder.value;
    }

    public static class ServiceSyncHelper {
        // The semaphore will wakeup clients
        protected final Semaphore semaphore = new Semaphore(0);

        /**
         * Waits for some response coming from the service.
         *
         * @param timeout
         *            The maximum time to wait.
         * @throws InterruptedException
         *             if the Thread is interrupted or reaches the timeout.
         */
        public synchronized void waitListener(long timeout) throws InterruptedException {
            if (!semaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS))
                throw new InterruptedException();
        }
    }

}