1/*
2 * Copyright (C) 2015 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 com.android.mtp;
18
19import android.content.Context;
20import android.database.Cursor;
21import android.mtp.MtpObjectInfo;
22import android.net.Uri;
23import android.provider.DocumentsContract;
24import android.provider.DocumentsContract.Document;
25import android.test.AndroidTestCase;
26import android.test.suitebuilder.annotation.MediumTest;
27
28import java.io.IOException;
29import java.util.HashMap;
30import java.util.Map;
31import java.util.concurrent.CountDownLatch;
32import java.util.concurrent.TimeoutException;
33
34@MediumTest
35public class DocumentLoaderTest extends AndroidTestCase {
36    private MtpDatabase mDatabase;
37    private BlockableTestMtpManager mManager;
38    private TestContentResolver mResolver;
39    private DocumentLoader mLoader;
40    final private Identifier mParentIdentifier = new Identifier(
41            0, 0, 0, "2", MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE);
42
43    @Override
44    public void setUp() throws Exception {
45        mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
46
47        mDatabase.getMapper().startAddingDocuments(null);
48        mDatabase.getMapper().putDeviceDocument(
49                new MtpDeviceRecord(0, "Device", null, true, new MtpRoot[0], null, null));
50        mDatabase.getMapper().stopAddingDocuments(null);
51
52        mDatabase.getMapper().startAddingDocuments("1");
53        mDatabase.getMapper().putStorageDocuments("1", new int[0], new MtpRoot[] {
54                new MtpRoot(0, 0, "Storage", 1000, 1000, "")
55        });
56        mDatabase.getMapper().stopAddingDocuments("1");
57
58        mManager = new BlockableTestMtpManager(getContext());
59        mResolver = new TestContentResolver();
60    }
61
62    @Override
63    public void tearDown() throws Exception {
64        mLoader.close();
65        mDatabase.close();
66    }
67
68    public void testBasic() throws Exception {
69        setUpLoader();
70
71        final Uri uri = DocumentsContract.buildChildDocumentsUri(
72                MtpDocumentsProvider.AUTHORITY, mParentIdentifier.mDocumentId);
73        setUpDocument(mManager, 40);
74        mManager.blockDocument(0, 15);
75        mManager.blockDocument(0, 35);
76
77        {
78            final Cursor cursor = mLoader.queryChildDocuments(
79                    MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier);
80            assertEquals(DocumentLoader.NUM_INITIAL_ENTRIES, cursor.getCount());
81        }
82
83        Thread.sleep(DocumentLoader.NOTIFY_PERIOD_MS);
84        mManager.unblockDocument(0, 15);
85        mResolver.waitForNotification(uri, 1);
86
87        {
88            final Cursor cursor = mLoader.queryChildDocuments(
89                    MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier);
90            assertEquals(
91                    DocumentLoader.NUM_INITIAL_ENTRIES + DocumentLoader.NUM_LOADING_ENTRIES,
92                    cursor.getCount());
93        }
94
95        mManager.unblockDocument(0, 35);
96        mResolver.waitForNotification(uri, 2);
97
98        {
99            final Cursor cursor = mLoader.queryChildDocuments(
100                    MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier);
101            assertEquals(40, cursor.getCount());
102        }
103
104        assertEquals(2, mResolver.getChangeCount(uri));
105    }
106
107    public void testError_GetObjectHandles() throws Exception {
108        mManager = new BlockableTestMtpManager(getContext()) {
109            @Override
110            int[] getObjectHandles(int deviceId, int storageId, int parentObjectHandle)
111                    throws IOException {
112                throw new IOException();
113            }
114        };
115        setUpLoader();
116        mManager.setObjectHandles(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, null);
117        try {
118            try (final Cursor cursor = mLoader.queryChildDocuments(
119                    MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier)) {}
120            fail();
121        } catch (IOException exception) {
122            // Expect exception.
123        }
124    }
125
126    public void testError_GetObjectInfo() throws Exception {
127        mManager = new BlockableTestMtpManager(getContext()) {
128            @Override
129            MtpObjectInfo getObjectInfo(int deviceId, int objectHandle) throws IOException {
130                if (objectHandle == DocumentLoader.NUM_INITIAL_ENTRIES) {
131                    throw new IOException();
132                } else {
133                    return super.getObjectInfo(deviceId, objectHandle);
134                }
135            }
136        };
137        setUpLoader();
138        setUpDocument(mManager, DocumentLoader.NUM_INITIAL_ENTRIES);
139        try (final Cursor cursor = mLoader.queryChildDocuments(
140                MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier)) {
141            // Even if MtpManager returns an error for a document, loading must complete.
142            assertFalse(cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING));
143        }
144    }
145
146    public void testCancelTask() throws IOException, InterruptedException, TimeoutException {
147        setUpDocument(mManager,
148                DocumentLoader.NUM_INITIAL_ENTRIES + 1);
149
150        // Block the first iteration in the background thread.
151        mManager.blockDocument(
152                0, DocumentLoader.NUM_INITIAL_ENTRIES + 1);
153        setUpLoader();
154        try (final Cursor cursor = mLoader.queryChildDocuments(
155                MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier)) {
156            assertTrue(cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING));
157        }
158
159        final Uri uri = DocumentsContract.buildChildDocumentsUri(
160                MtpDocumentsProvider.AUTHORITY, mParentIdentifier.mDocumentId);
161        assertEquals(0, mResolver.getChangeCount(uri));
162
163        // Clear task while the first iteration is being blocked.
164        mLoader.cancelTask(mParentIdentifier);
165        mManager.unblockDocument(
166                0, DocumentLoader.NUM_INITIAL_ENTRIES + 1);
167        Thread.sleep(DocumentLoader.NOTIFY_PERIOD_MS);
168        assertEquals(0, mResolver.getChangeCount(uri));
169
170        // Check if it's OK to query invalidated task.
171        try (final Cursor cursor = mLoader.queryChildDocuments(
172                MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier)) {
173            assertTrue(cursor.getExtras().getBoolean(DocumentsContract.EXTRA_LOADING));
174        }
175        mResolver.waitForNotification(uri, 1);
176    }
177
178    private void setUpLoader() {
179        mLoader = new DocumentLoader(
180                new MtpDeviceRecord(
181                        0, "Device", "Key", true, new MtpRoot[0],
182                        TestUtil.OPERATIONS_SUPPORTED, new int[0]),
183                mManager,
184                mResolver,
185                mDatabase);
186    }
187
188    private void setUpDocument(TestMtpManager manager, int count) {
189        int[] childDocuments = new int[count];
190        for (int i = 0; i < childDocuments.length; i++) {
191            final int objectHandle = i + 1;
192            childDocuments[i] = objectHandle;
193            manager.setObjectInfo(0, new MtpObjectInfo.Builder()
194                    .setObjectHandle(objectHandle)
195                    .setName(Integer.toString(i))
196                    .build());
197        }
198        manager.setObjectHandles(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, childDocuments);
199    }
200
201    private static class BlockableTestMtpManager extends TestMtpManager {
202        final private Map<String, CountDownLatch> blockedDocuments = new HashMap<>();
203
204        BlockableTestMtpManager(Context context) {
205            super(context);
206        }
207
208        void blockDocument(int deviceId, int objectHandle) {
209            blockedDocuments.put(pack(deviceId, objectHandle), new CountDownLatch(1));
210        }
211
212        void unblockDocument(int deviceId, int objectHandle) {
213            blockedDocuments.get(pack(deviceId, objectHandle)).countDown();
214        }
215
216        @Override
217        MtpObjectInfo getObjectInfo(int deviceId, int objectHandle) throws IOException {
218            final CountDownLatch latch = blockedDocuments.get(pack(deviceId, objectHandle));
219            if (latch != null) {
220                try {
221                    latch.await();
222                } catch(InterruptedException e) {
223                    fail();
224                }
225            }
226            return super.getObjectInfo(deviceId, objectHandle);
227        }
228    }
229}
230