1 /* 2 * Copyright (C) 2010 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 17 package com.android.email.provider; 18 19 import com.android.email.provider.ContentCache.CacheToken; 20 import com.android.email.provider.ContentCache.CachedCursor; 21 import com.android.email.provider.ContentCache.TokenList; 22 import com.android.emailcommon.provider.Account; 23 import com.android.emailcommon.provider.EmailContent; 24 import com.android.emailcommon.provider.Mailbox; 25 26 import android.content.ContentResolver; 27 import android.content.ContentUris; 28 import android.content.Context; 29 import android.database.Cursor; 30 import android.database.CursorWrapper; 31 import android.database.MatrixCursor; 32 import android.net.Uri; 33 import android.test.ProviderTestCase2; 34 35 /** 36 * Tests of ContentCache 37 * 38 * You can run this entire test case with: 39 * runtest -c com.android.email.provider.ContentCacheTests email 40 */ 41 public class ContentCacheTests extends ProviderTestCase2<EmailProvider> { 42 43 EmailProvider mProvider; 44 Context mMockContext; 45 46 public ContentCacheTests() { 47 super(EmailProvider.class, EmailContent.AUTHORITY); 48 } 49 50 @Override 51 public void setUp() throws Exception { 52 super.setUp(); 53 mMockContext = getMockContext(); 54 } 55 56 @Override 57 public void tearDown() throws Exception { 58 super.tearDown(); 59 } 60 61 public void testCounterMap() { 62 ContentCache.CounterMap<String> map = new ContentCache.CounterMap<String>(4); 63 // Make sure we can find added items 64 map.add("1"); 65 assertTrue(map.contains("1")); 66 map.add("2"); 67 map.add("2"); 68 // Make sure we can remove once for each add 69 map.subtract("2"); 70 assertTrue(map.contains("2")); 71 map.subtract("2"); 72 // Make sure that over-removing throws an exception 73 try { 74 map.subtract("2"); 75 fail("Removing a third time should throw an exception"); 76 } catch (IllegalStateException e) { 77 } 78 try { 79 map.subtract("3"); 80 fail("Removing object never added should throw an exception"); 81 } catch (IllegalStateException e) { 82 } 83 // There should only be one item in the map ("1") 84 assertEquals(1, map.size()); 85 assertTrue(map.contains("1")); 86 } 87 88 public void testTokenList() { 89 TokenList list = new TokenList("Name"); 90 91 // Add two tokens for "1" 92 CacheToken token1a = list.add("1"); 93 assertTrue(token1a.isValid()); 94 assertEquals("1", token1a.getId()); 95 assertEquals(1, list.size()); 96 CacheToken token1b = list.add("1"); 97 assertTrue(token1b.isValid()); 98 assertEquals("1", token1b.getId()); 99 assertTrue(token1a.equals(token1b)); 100 assertEquals(2, list.size()); 101 102 // Add a token for "2" 103 CacheToken token2 = list.add("2"); 104 assertFalse(token1a.equals(token2)); 105 assertEquals(3, list.size()); 106 107 // Invalidate "1"; there should be two tokens invalidated 108 assertEquals(2, list.invalidateTokens("1")); 109 assertFalse(token1a.isValid()); 110 assertFalse(token1b.isValid()); 111 // Token2 should still be valid 112 assertTrue(token2.isValid()); 113 // Only token2 should be in the list now (invalidation removes tokens) 114 assertEquals(1, list.size()); 115 assertEquals(token2, list.get(0)); 116 117 // Add 3 tokens for "3" 118 CacheToken token3a = list.add("3"); 119 CacheToken token3b = list.add("3"); 120 CacheToken token3c = list.add("3"); 121 // Remove two of them 122 assertTrue(list.remove(token3a)); 123 assertTrue(list.remove(token3b)); 124 // Removing tokens doesn't invalidate them 125 assertTrue(token3a.isValid()); 126 assertTrue(token3b.isValid()); 127 assertTrue(token3c.isValid()); 128 // There should be two items left "3" and "2" 129 assertEquals(2, list.size()); 130 } 131 132 public void testCachedCursors() { 133 final ContentResolver resolver = mMockContext.getContentResolver(); 134 final Context context = mMockContext; 135 136 // Create account and two mailboxes 137 Account acct = ProviderTestUtils.setupAccount("account", true, context); 138 ProviderTestUtils.setupMailbox("box1", acct.mId, true, context); 139 Mailbox box = ProviderTestUtils.setupMailbox("box2", acct.mId, true, context); 140 141 // We need to test with a query that only returns one row (others can't be put in a 142 // CachedCursor) 143 Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, box.mId); 144 Cursor cursor = 145 resolver.query(uri, Mailbox.CONTENT_PROJECTION, null, null, null); 146 // ContentResolver gives us back a wrapper 147 assertTrue(cursor instanceof CursorWrapper); 148 // The wrappedCursor should be a CachedCursor 149 Cursor wrappedCursor = ((CursorWrapper)cursor).getWrappedCursor(); 150 assertTrue(wrappedCursor instanceof CachedCursor); 151 CachedCursor cachedCursor = (CachedCursor)wrappedCursor; 152 // The cursor wrapped in cachedCursor is the underlying cursor 153 Cursor activeCursor = cachedCursor.getWrappedCursor(); 154 155 // The cursor should be in active cursors 156 int activeCount = ContentCache.sActiveCursors.getCount(activeCursor); 157 assertEquals(1, activeCount); 158 159 // Some basic functionality that shouldn't throw exceptions and should otherwise act as the 160 // underlying cursor would 161 String[] columnNames = cursor.getColumnNames(); 162 assertEquals(Mailbox.CONTENT_PROJECTION.length, columnNames.length); 163 for (int i = 0; i < Mailbox.CONTENT_PROJECTION.length; i++) { 164 assertEquals(Mailbox.CONTENT_PROJECTION[i], columnNames[i]); 165 } 166 167 assertEquals(1, cursor.getCount()); 168 cursor.moveToNext(); 169 assertEquals(0, cursor.getPosition()); 170 cursor.moveToPosition(0); 171 assertEquals(0, cursor.getPosition()); 172 assertFalse(cursor.moveToPosition(1)); 173 174 cursor.close(); 175 // We've closed the cached cursor; make sure 176 assertTrue(cachedCursor.isClosed()); 177 // The underlying cursor shouldn't be closed because it's in a cache (we'll test 178 // that in testContentCache) 179 assertFalse(activeCursor.isClosed()); 180 // Our cursor should no longer be in the active cursors map 181 assertFalse(ContentCache.sActiveCursors.contains(activeCursor)); 182 183 // TODO - change the code or the test to enforce the assertion that a cached cursor 184 // should have only zero or one rows. We cannot test this in the constructor, however, 185 // due to potential for deadlock. 186 // // Make sure that we won't accept cursors with multiple rows 187 // cursor = resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, null, null, null); 188 // try { 189 // cursor = new CachedCursor(cursor, null, "Foo"); 190 // fail("Mustn't accept cursor with more than one row"); 191 // } catch (IllegalArgumentException e) { 192 // // Correct 193 // } 194 } 195 196 private static final String[] SIMPLE_PROJECTION = new String[] {"Foo"}; 197 private static final Object[] SIMPLE_ROW = new Object[] {"Bar"}; 198 private Cursor getOneRowCursor() { 199 MatrixCursor cursor = new MatrixCursor(SIMPLE_PROJECTION, 1); 200 cursor.addRow(SIMPLE_ROW); 201 return cursor; 202 } 203 204 public void testContentCacheRemoveEldestEntry() { 205 // Create a cache of size 2 206 ContentCache cache = new ContentCache("Name", SIMPLE_PROJECTION, 2); 207 // Random cursor; what's in it doesn't matter 208 Cursor cursor1 = getOneRowCursor(); 209 // Get a token for arbitrary object named "1" 210 CacheToken token = cache.getCacheToken("1"); 211 // Put the cursor in the cache 212 cache.putCursor(cursor1, "1", SIMPLE_PROJECTION, token); 213 assertEquals(1, cache.size()); 214 215 // Add another random cursor; what's in it doesn't matter 216 Cursor cursor2 = getOneRowCursor(); 217 // Get a token for arbitrary object named "2" 218 token = cache.getCacheToken("2"); 219 // Put the cursor in the cache 220 cache.putCursor(cursor1, "2", SIMPLE_PROJECTION, token); 221 assertEquals(2, cache.size()); 222 223 // We should be able to find both now in the cache 224 Cursor cachedCursor = cache.getCachedCursor("1", SIMPLE_PROJECTION); 225 assertNotNull(cachedCursor); 226 assertTrue(cachedCursor instanceof CachedCursor); 227 cachedCursor = cache.getCachedCursor("2", SIMPLE_PROJECTION); 228 assertNotNull(cachedCursor); 229 assertTrue(cachedCursor instanceof CachedCursor); 230 231 // Both cursors should be open 232 assertFalse(cursor1.isClosed()); 233 assertFalse(cursor2.isClosed()); 234 235 // Add another random cursor; what's in it doesn't matter 236 Cursor cursor3 = getOneRowCursor(); 237 // Get a token for arbitrary object named "3" 238 token = cache.getCacheToken("3"); 239 // Put the cursor in the cache 240 cache.putCursor(cursor1, "3", SIMPLE_PROJECTION, token); 241 // We should never have more than 2 entries in the cache 242 assertEquals(2, cache.size()); 243 244 // The first cursor we added should no longer be in the cache (it's the eldest) 245 cachedCursor = cache.getCachedCursor("1", SIMPLE_PROJECTION); 246 assertNull(cachedCursor); 247 // The cursors for 2 and 3 should be cached 248 cachedCursor = cache.getCachedCursor("2", SIMPLE_PROJECTION); 249 assertNotNull(cachedCursor); 250 assertTrue(cachedCursor instanceof CachedCursor); 251 cachedCursor = cache.getCachedCursor("3", SIMPLE_PROJECTION); 252 assertNotNull(cachedCursor); 253 assertTrue(cachedCursor instanceof CachedCursor); 254 255 // Even cursor1 should be open, since all cached cursors are in mActiveCursors until closed 256 assertFalse(cursor1.isClosed()); 257 assertFalse(cursor2.isClosed()); 258 assertFalse(cursor3.isClosed()); 259 } 260 261 public void testCloseCachedCursor() { 262 // Create a cache of size 2 263 ContentCache cache = new ContentCache("Name", SIMPLE_PROJECTION, 2); 264 // Random cursor; what's in it doesn't matter 265 Cursor underlyingCursor = getOneRowCursor(); 266 Cursor cachedCursor1 = new CachedCursor(underlyingCursor, cache, "1"); 267 Cursor cachedCursor2 = new CachedCursor(underlyingCursor, cache, "1"); 268 assertEquals(2, ContentCache.sActiveCursors.getCount(underlyingCursor)); 269 cachedCursor1.close(); 270 assertTrue(cachedCursor1.isClosed()); 271 // Underlying cursor should be open (still one cached cursor open) 272 assertFalse(underlyingCursor.isClosed()); 273 cachedCursor2.close(); 274 assertTrue(cachedCursor2.isClosed()); 275 assertEquals(0, ContentCache.sActiveCursors.getCount(underlyingCursor)); 276 // Underlying cursor should be closed (no cached cursors open) 277 assertTrue(underlyingCursor.isClosed()); 278 279 underlyingCursor = getOneRowCursor(); 280 cachedCursor1 = cache.putCursor( 281 underlyingCursor, "2", SIMPLE_PROJECTION, cache.getCacheToken("2")); 282 cachedCursor2 = new CachedCursor(underlyingCursor, cache, "2"); 283 assertEquals(2, ContentCache.sActiveCursors.getCount(underlyingCursor)); 284 cachedCursor1.close(); 285 cachedCursor2.close(); 286 assertEquals(0, ContentCache.sActiveCursors.getCount(underlyingCursor)); 287 // Underlying cursor should still be open; it's in the cache 288 assertFalse(underlyingCursor.isClosed()); 289 // Cache a new cursor 290 cachedCursor2 = new CachedCursor(underlyingCursor, cache, "2"); 291 assertEquals(1, ContentCache.sActiveCursors.getCount(underlyingCursor)); 292 // Remove "2" from the cache and close the cursor 293 cache.invalidate(); 294 cachedCursor2.close(); 295 // The underlying cursor should now be closed (not in the cache and no cached cursors) 296 assertEquals(0, ContentCache.sActiveCursors.getCount(underlyingCursor)); 297 assertTrue(underlyingCursor.isClosed()); 298 } 299 } 300