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