1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.loader.app;
18
19import static org.junit.Assert.assertFalse;
20import static org.junit.Assert.assertTrue;
21import static org.junit.Assert.fail;
22import static org.mockito.Mockito.mock;
23
24import android.content.Context;
25import android.os.Bundle;
26import android.support.test.InstrumentationRegistry;
27import android.support.test.annotation.UiThreadTest;
28import android.support.test.filters.SmallTest;
29import android.support.test.runner.AndroidJUnit4;
30
31import androidx.annotation.NonNull;
32import androidx.lifecycle.Lifecycle;
33import androidx.lifecycle.LifecycleOwner;
34import androidx.lifecycle.LifecycleRegistry;
35import androidx.lifecycle.ViewModelStore;
36import androidx.lifecycle.ViewModelStoreOwner;
37import androidx.loader.app.test.DelayLoaderCallbacks;
38import androidx.loader.app.test.DummyLoaderCallbacks;
39import androidx.loader.content.Loader;
40
41import org.junit.Before;
42import org.junit.Test;
43import org.junit.runner.RunWith;
44
45import java.util.concurrent.CountDownLatch;
46import java.util.concurrent.TimeUnit;
47
48@RunWith(AndroidJUnit4.class)
49@SmallTest
50public class LoaderManagerTest {
51
52    private LoaderManager mLoaderManager;
53
54    @Before
55    public void setup() {
56        mLoaderManager = LoaderManager.getInstance(new LoaderOwner());
57    }
58
59    @Test
60    public void testDestroyFromOnCreateLoader() throws Throwable {
61        final CountDownLatch onCreateLoaderLatch = new CountDownLatch(1);
62        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
63            @Override
64            public void run() {
65                mLoaderManager.initLoader(65, null,
66                        new DummyLoaderCallbacks(mock(Context.class)) {
67                            @NonNull
68                            @Override
69                            public Loader<Boolean> onCreateLoader(int id, Bundle args) {
70                                try {
71                                    mLoaderManager.destroyLoader(65);
72                                    fail("Calling destroyLoader in onCreateLoader should throw an "
73                                            + "IllegalStateException");
74                                } catch (IllegalStateException e) {
75                                    // Expected
76                                    onCreateLoaderLatch.countDown();
77                                }
78                                return super.onCreateLoader(id, args);
79                            }
80                        });
81            }
82        });
83        onCreateLoaderLatch.await(1, TimeUnit.SECONDS);
84    }
85
86    /**
87     * Test to ensure that loader operations, such as destroyLoader, can safely be called
88     * in onLoadFinished
89     */
90    @Test
91    public void testDestroyFromOnLoadFinished() throws Throwable {
92        final CountDownLatch onLoadFinishedLatch = new CountDownLatch(1);
93        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
94            @Override
95            public void run() {
96                mLoaderManager.initLoader(43, null,
97                        new DummyLoaderCallbacks(mock(Context.class)) {
98                            @Override
99                            public void onLoadFinished(@NonNull Loader<Boolean> loader,
100                                    Boolean data) {
101                                super.onLoadFinished(loader, data);
102                                mLoaderManager.destroyLoader(43);
103                            }
104                        });
105            }
106        });
107        onLoadFinishedLatch.await(1, TimeUnit.SECONDS);
108    }
109
110    @Test
111    public void testDestroyLoaderBeforeDeliverData() throws Throwable {
112        final DelayLoaderCallbacks callback =
113                new DelayLoaderCallbacks(mock(Context.class), new CountDownLatch(1));
114        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
115            @Override
116            public void run() {
117                mLoaderManager.initLoader(37, null, callback);
118                // Immediately destroy it before it has a chance to deliver data
119                mLoaderManager.destroyLoader(37);
120            }
121        });
122        assertFalse("LoaderCallbacks should not be reset if they never received data",
123                callback.mOnLoaderReset);
124        assertTrue("Loader should be reset after destroyLoader()",
125                callback.mLoader.isReset());
126    }
127
128    @Test
129    public void testDestroyLoaderAfterDeliverData() throws Throwable {
130        CountDownLatch countDownLatch = new CountDownLatch(1);
131        final DelayLoaderCallbacks callback =
132                new DelayLoaderCallbacks(mock(Context.class), countDownLatch);
133        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
134            @Override
135            public void run() {
136                mLoaderManager.initLoader(38, null, callback);
137            }
138        });
139        // Wait for the Loader to return data
140        countDownLatch.await(1, TimeUnit.SECONDS);
141        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
142            @Override
143            public void run() {
144                mLoaderManager.destroyLoader(38);
145            }
146        });
147        assertTrue("LoaderCallbacks should be reset after destroyLoader()",
148                callback.mOnLoaderReset);
149        assertTrue("Loader should be reset after destroyLoader()",
150                callback.mLoader.isReset());
151    }
152
153
154    @Test
155    public void testRestartLoaderBeforeDeliverData() throws Throwable {
156        final DelayLoaderCallbacks initialCallback =
157                new DelayLoaderCallbacks(mock(Context.class), new CountDownLatch(1));
158        CountDownLatch restartCountDownLatch = new CountDownLatch(1);
159        final DelayLoaderCallbacks restartCallback =
160                new DelayLoaderCallbacks(mock(Context.class), restartCountDownLatch);
161        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
162            @Override
163            public void run() {
164                mLoaderManager.initLoader(44, null, initialCallback);
165                // Immediately restart it before it has a chance to deliver data
166                mLoaderManager.restartLoader(44, null, restartCallback);
167            }
168        });
169        assertFalse("Initial LoaderCallbacks should not be reset after restartLoader()",
170                initialCallback.mOnLoaderReset);
171        assertTrue("Initial Loader should be reset if it is restarted before delivering data",
172                initialCallback.mLoader.isReset());
173        restartCountDownLatch.await(1, TimeUnit.SECONDS);
174    }
175
176    @Test
177    public void testRestartLoaderAfterDeliverData() throws Throwable {
178        CountDownLatch initialCountDownLatch = new CountDownLatch(1);
179        final DelayLoaderCallbacks initialCallback =
180                new DelayLoaderCallbacks(mock(Context.class), initialCountDownLatch);
181        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
182            @Override
183            public void run() {
184                mLoaderManager.initLoader(45, null, initialCallback);
185            }
186        });
187        // Wait for the first Loader to return data
188        initialCountDownLatch.await(1, TimeUnit.SECONDS);
189        CountDownLatch restartCountDownLatch = new CountDownLatch(1);
190        final DelayLoaderCallbacks restartCallback =
191                new DelayLoaderCallbacks(mock(Context.class), restartCountDownLatch);
192        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
193            @Override
194            public void run() {
195                mLoaderManager.restartLoader(45, null, restartCallback);
196            }
197        });
198        assertFalse("Initial LoaderCallbacks should not be reset after restartLoader()",
199                initialCallback.mOnLoaderReset);
200        assertFalse("Initial Loader should not be reset if it is restarted after delivering data",
201                initialCallback.mLoader.isReset());
202        restartCountDownLatch.await(1, TimeUnit.SECONDS);
203        assertTrue("Initial Loader should be reset after its replacement Loader delivers data",
204                initialCallback.mLoader.isReset());
205    }
206
207    @Test
208    public void testRestartLoaderMultiple() throws Throwable {
209        CountDownLatch initialCountDownLatch = new CountDownLatch(1);
210        final DelayLoaderCallbacks initialCallback =
211                new DelayLoaderCallbacks(mock(Context.class), initialCountDownLatch);
212        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
213            @Override
214            public void run() {
215                mLoaderManager.initLoader(46, null, initialCallback);
216            }
217        });
218        // Wait for the first Loader to return data
219        initialCountDownLatch.await(1, TimeUnit.SECONDS);
220        final DelayLoaderCallbacks intermediateCallback =
221                new DelayLoaderCallbacks(mock(Context.class), new CountDownLatch(1));
222        CountDownLatch restartCountDownLatch = new CountDownLatch(1);
223        final DelayLoaderCallbacks restartCallback =
224                new DelayLoaderCallbacks(mock(Context.class), restartCountDownLatch);
225        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
226            @Override
227            public void run() {
228                mLoaderManager.restartLoader(46, null, intermediateCallback);
229                // Immediately replace the restarted Loader with yet another Loader
230                mLoaderManager.restartLoader(46, null, restartCallback);
231            }
232        });
233        assertFalse("Initial LoaderCallbacks should not be reset after restartLoader()",
234                initialCallback.mOnLoaderReset);
235        assertFalse("Initial Loader should not be reset if it is restarted after delivering data",
236                initialCallback.mLoader.isReset());
237        assertTrue("Intermediate Loader should be reset if it is restarted before delivering data",
238                intermediateCallback.mLoader.isReset());
239        restartCountDownLatch.await(1, TimeUnit.SECONDS);
240        assertTrue("Initial Loader should be reset after its replacement Loader delivers data",
241                initialCallback.mLoader.isReset());
242    }
243
244    @UiThreadTest
245    @Test(expected = IllegalArgumentException.class)
246    public void enforceNonNullLoader() {
247        mLoaderManager.initLoader(-1, null, new LoaderManager.LoaderCallbacks<Object>() {
248            @Override
249            public Loader<Object> onCreateLoader(int id, Bundle args) {
250                return null;
251            }
252
253            @Override
254            public void onLoadFinished(Loader<Object> loader, Object data) {
255            }
256
257            @Override
258            public void onLoaderReset(Loader<Object> loader) {
259            }
260        });
261    }
262
263    @Test(expected = IllegalStateException.class)
264    public void enforceOnMainThread_initLoader() {
265        mLoaderManager.initLoader(-1, null,
266                new DummyLoaderCallbacks(mock(Context.class)));
267    }
268
269    @Test(expected = IllegalStateException.class)
270    public void enforceOnMainThread_restartLoader() {
271        mLoaderManager.restartLoader(-1, null,
272                new DummyLoaderCallbacks(mock(Context.class)));
273    }
274
275    @Test(expected = IllegalStateException.class)
276    public void enforceOnMainThread_destroyLoader() {
277        mLoaderManager.destroyLoader(-1);
278    }
279
280    class LoaderOwner implements LifecycleOwner, ViewModelStoreOwner {
281
282        private LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
283        private ViewModelStore mViewModelStore = new ViewModelStore();
284
285        LoaderOwner() {
286            mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START);
287        }
288
289        @NonNull
290        @Override
291        public Lifecycle getLifecycle() {
292            return mLifecycle;
293        }
294
295        @NonNull
296        @Override
297        public ViewModelStore getViewModelStore() {
298            return mViewModelStore;
299        }
300    }
301}
302