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 
17 package com.android.documentsui;
18 
19 import android.content.ContentProviderClient;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.ContextWrapper;
23 import android.content.Intent;
24 import android.content.pm.ProviderInfo;
25 import android.database.ContentObserver;
26 import android.database.Cursor;
27 import android.net.Uri;
28 import android.os.Parcelable;
29 import android.os.RemoteException;
30 import android.provider.DocumentsContract;
31 import android.test.MoreAsserts;
32 import android.test.ServiceTestCase;
33 import android.test.mock.MockContentResolver;
34 import android.util.Log;
35 
36 import com.android.documentsui.model.DocumentInfo;
37 import com.android.documentsui.model.DocumentStack;
38 import com.android.documentsui.model.RootInfo;
39 import com.google.common.collect.Lists;
40 
41 import libcore.io.IoUtils;
42 import libcore.io.Streams;
43 
44 import java.io.File;
45 import java.io.FileInputStream;
46 import java.io.FileNotFoundException;
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.concurrent.CountDownLatch;
50 import java.util.concurrent.TimeUnit;
51 import java.util.concurrent.TimeoutException;
52 
53 public class CopyTest extends ServiceTestCase<CopyService> {
54 
55     /**
56      * A test resolver that enables this test suite to listen for notifications that mark when copy
57      * operations are done.
58      */
59     class TestContentResolver extends MockContentResolver {
60         private CountDownLatch mReadySignal;
61         private CountDownLatch mNotificationSignal;
62 
TestContentResolver()63         public TestContentResolver() {
64             mReadySignal = new CountDownLatch(1);
65         }
66 
67         /**
68          * Wait for the given number of files to be copied to destination. Times out after 1 sec.
69          */
waitForChanges(int count)70         public void waitForChanges(int count) throws Exception {
71             // Wait for no more than 1 second by default.
72             waitForChanges(count, 1000);
73         }
74 
75         /**
76          * Wait for files to be copied to destination.
77          *
78          * @param count Number of files to wait for.
79          * @param timeOut Timeout in ms. TimeoutException will be thrown if this function times out.
80          */
waitForChanges(int count, int timeOut)81         public void waitForChanges(int count, int timeOut) throws Exception {
82             mNotificationSignal = new CountDownLatch(count);
83             // Signal that the test is now waiting for files.
84             mReadySignal.countDown();
85             if (!mNotificationSignal.await(timeOut, TimeUnit.MILLISECONDS)) {
86                 throw new TimeoutException("Timed out waiting for files to be copied.");
87             }
88         }
89 
90         @Override
notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork)91         public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
92             // Wait until the test is ready to receive file notifications.
93             try {
94                 mReadySignal.await();
95             } catch (InterruptedException e) {
96                 Log.d(TAG, "Interrupted while waiting for file copy readiness");
97                 Thread.currentThread().interrupt();
98             }
99             if (DocumentsContract.isDocumentUri(mContext, uri)) {
100                 Log.d(TAG, "Notification: " + uri);
101                 // Watch for document URI change notifications - this signifies the end of a copy.
102                 mNotificationSignal.countDown();
103             }
104         }
105     };
106 
CopyTest()107     public CopyTest() {
108         super(CopyService.class);
109     }
110 
111     private static String AUTHORITY = "com.android.documentsui.stubprovider";
112     private static String DST = "sd1";
113     private static String SRC = "sd0";
114     private static String TAG = "CopyTest";
115     private List<RootInfo> mRoots;
116     private Context mContext;
117     private TestContentResolver mResolver;
118     private ContentProviderClient mClient;
119     private StubProvider mStorage;
120     private Context mSystemContext;
121 
122     @Override
setUp()123     protected void setUp() throws Exception {
124         super.setUp();
125 
126         setupTestContext();
127         mClient = mResolver.acquireContentProviderClient(AUTHORITY);
128 
129         // Reset the stub provider's storage.
130         mStorage.clearCacheAndBuildRoots();
131 
132         mRoots = Lists.newArrayList();
133         Uri queryUri = DocumentsContract.buildRootsUri(AUTHORITY);
134         Cursor cursor = null;
135         try {
136             cursor = mClient.query(queryUri, null, null, null, null);
137             while (cursor.moveToNext()) {
138                 mRoots.add(RootInfo.fromRootsCursor(AUTHORITY, cursor));
139             }
140         } finally {
141             IoUtils.closeQuietly(cursor);
142         }
143 
144     }
145 
146     @Override
tearDown()147     protected void tearDown() throws Exception {
148         mClient.release();
149         super.tearDown();
150     }
151 
152     /**
153      * Test copying a single file.
154      */
testCopyFile()155     public void testCopyFile() throws Exception {
156         String srcPath = "/test0.txt";
157         Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain",
158                 "The five boxing wizards jump quickly".getBytes());
159 
160         assertDstFileCountEquals(0);
161 
162         copyToDestination(Lists.newArrayList(testFile));
163 
164         // 2 operations: file creation, then writing data.
165         mResolver.waitForChanges(2);
166 
167         // Verify that one file was copied; check file contents.
168         assertDstFileCountEquals(1);
169         assertCopied(srcPath);
170     }
171 
172     /**
173      * Test copying multiple files.
174      */
testCopyMultipleFiles()175     public void testCopyMultipleFiles() throws Exception {
176         String testContent[] = {
177                 "The five boxing wizards jump quickly",
178                 "The quick brown fox jumps over the lazy dog",
179                 "Jackdaws love my big sphinx of quartz"
180         };
181         String srcPaths[] = {
182                 "/test0.txt",
183                 "/test1.txt",
184                 "/test2.txt"
185         };
186         List<Uri> testFiles = Lists.newArrayList(
187                 mStorage.createFile(SRC, srcPaths[0], "text/plain", testContent[0].getBytes()),
188                 mStorage.createFile(SRC, srcPaths[1], "text/plain", testContent[1].getBytes()),
189                 mStorage.createFile(SRC, srcPaths[2], "text/plain", testContent[2].getBytes()));
190 
191         assertDstFileCountEquals(0);
192 
193         // Copy all the test files.
194         copyToDestination(testFiles);
195 
196         // 3 file creations, 3 file writes.
197         mResolver.waitForChanges(6);
198 
199         assertDstFileCountEquals(3);
200         for (String path : srcPaths) {
201             assertCopied(path);
202         }
203     }
204 
testCopyEmptyDir()205     public void testCopyEmptyDir() throws Exception {
206         String srcPath = "/emptyDir";
207         Uri testDir = mStorage.createFile(SRC, srcPath, DocumentsContract.Document.MIME_TYPE_DIR,
208                 null);
209 
210         assertDstFileCountEquals(0);
211 
212         copyToDestination(Lists.newArrayList(testDir));
213 
214         // Just 1 operation: Directory creation.
215         mResolver.waitForChanges(1);
216 
217         assertDstFileCountEquals(1);
218 
219         File dst = mStorage.getFile(DST, srcPath);
220         assertTrue(dst.isDirectory());
221     }
222 
testReadErrors()223     public void testReadErrors() throws Exception {
224         String srcPath = "/test0.txt";
225         Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain",
226                 "The five boxing wizards jump quickly".getBytes());
227 
228         assertDstFileCountEquals(0);
229 
230         mStorage.simulateReadErrors(true);
231 
232         copyToDestination(Lists.newArrayList(testFile));
233 
234         // 3 operations: file creation, writing, then deletion (due to failed copy).
235         mResolver.waitForChanges(3);
236 
237         assertDstFileCountEquals(0);
238     }
239 
240     /**
241      * Copies the given files to a pre-determined destination.
242      *
243      * @throws FileNotFoundException
244      */
copyToDestination(List<Uri> srcs)245     private void copyToDestination(List<Uri> srcs) throws FileNotFoundException {
246         final ArrayList<DocumentInfo> srcDocs = Lists.newArrayList();
247         for (Uri src : srcs) {
248             srcDocs.add(DocumentInfo.fromUri(mResolver, src));
249         }
250 
251         final Uri dst = DocumentsContract.buildDocumentUri(AUTHORITY, mRoots.get(1).documentId);
252         DocumentStack stack = new DocumentStack();
253         stack.push(DocumentInfo.fromUri(mResolver, dst));
254         final Intent copyIntent = new Intent(mContext, CopyService.class);
255         copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST, srcDocs);
256         copyIntent.putExtra(CopyService.EXTRA_STACK, (Parcelable) stack);
257 
258         startService(copyIntent);
259     }
260 
261     /**
262      * Returns a count of the files in the given directory.
263      */
assertDstFileCountEquals(int expected)264     private void assertDstFileCountEquals(int expected) throws RemoteException {
265         final Uri queryUri = DocumentsContract.buildChildDocumentsUri(AUTHORITY,
266                 mRoots.get(1).documentId);
267         Cursor c = null;
268         int count = 0;
269         try {
270             c = mClient.query(queryUri, null, null, null, null);
271             count = c.getCount();
272         } finally {
273             IoUtils.closeQuietly(c);
274         }
275         assertEquals("Incorrect file count after copy", expected, count);
276     }
277 
assertCopied(String path)278     private void assertCopied(String path) throws Exception {
279         File srcFile = mStorage.getFile(SRC, path);
280         File dstFile = mStorage.getFile(DST, path);
281         assertNotNull(dstFile);
282 
283         FileInputStream src = null;
284         FileInputStream dst = null;
285         try {
286             src = new FileInputStream(srcFile);
287             dst = new FileInputStream(dstFile);
288             byte[] srcbuf = Streams.readFully(src);
289             byte[] dstbuf = Streams.readFully(dst);
290 
291             MoreAsserts.assertEquals(srcbuf, dstbuf);
292         } finally {
293             IoUtils.closeQuietly(src);
294             IoUtils.closeQuietly(dst);
295         }
296     }
297 
298     /**
299      * Sets up a ContextWrapper that substitutes a stub NotificationManager. This allows the test to
300      * listen for notification events, to gauge copy progress.
301      *
302      * @throws FileNotFoundException
303      */
setupTestContext()304     private void setupTestContext() throws FileNotFoundException {
305         mSystemContext = getSystemContext();
306 
307         // Set up the context with the test content resolver.
308         mResolver = new TestContentResolver();
309         mContext = new ContextWrapper(mSystemContext) {
310             @Override
311             public ContentResolver getContentResolver() {
312                 return mResolver;
313             }
314         };
315         setContext(mContext);
316 
317         // Create a local stub provider and add it to the content resolver.
318         ProviderInfo info = new ProviderInfo();
319         info.authority = AUTHORITY;
320         info.exported = true;
321         info.grantUriPermissions = true;
322         info.readPermission = android.Manifest.permission.MANAGE_DOCUMENTS;
323         info.writePermission = android.Manifest.permission.MANAGE_DOCUMENTS;
324 
325         mStorage = new StubProvider();
326         mStorage.attachInfo(mContext, info);
327         mResolver.addProvider(AUTHORITY, mStorage);
328     }
329 }
330