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