TestDocumentsProvider.java revision de2b22fbc60b29dd8af60cf05862066c04559dc0
1/*
2 * Copyright (C) 2013 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.externalstorage;
18
19import android.content.ContentResolver;
20import android.content.res.AssetFileDescriptor;
21import android.database.Cursor;
22import android.database.MatrixCursor;
23import android.database.MatrixCursor.RowBuilder;
24import android.graphics.Bitmap;
25import android.graphics.Bitmap.CompressFormat;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.Paint;
29import android.graphics.Point;
30import android.net.Uri;
31import android.os.AsyncTask;
32import android.os.Bundle;
33import android.os.CancellationSignal;
34import android.os.ParcelFileDescriptor;
35import android.os.SystemClock;
36import android.provider.DocumentsContract;
37import android.provider.DocumentsContract.Document;
38import android.provider.DocumentsContract.Root;
39import android.provider.DocumentsProvider;
40import android.util.Log;
41
42import libcore.io.IoUtils;
43import libcore.io.Streams;
44
45import java.io.ByteArrayInputStream;
46import java.io.ByteArrayOutputStream;
47import java.io.FileNotFoundException;
48import java.io.FileOutputStream;
49import java.io.IOException;
50import java.lang.ref.WeakReference;
51
52public class TestDocumentsProvider extends DocumentsProvider {
53    private static final String TAG = "TestDocuments";
54
55    private static final boolean CRASH_ROOTS = false;
56    private static final boolean CRASH_DOCUMENT = false;
57
58    private static final String MY_ROOT_ID = "myRoot";
59    private static final String MY_DOC_ID = "myDoc";
60    private static final String MY_DOC_NULL = "myNull";
61
62    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
63            Root.COLUMN_ROOT_ID, Root.COLUMN_ROOT_TYPE, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
64            Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
65            Root.COLUMN_AVAILABLE_BYTES,
66    };
67
68    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
69            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
70            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
71    };
72
73    private static String[] resolveRootProjection(String[] projection) {
74        return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
75    }
76
77    private static String[] resolveDocumentProjection(String[] projection) {
78        return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
79    }
80
81    @Override
82    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
83        if (CRASH_ROOTS) System.exit(12);
84
85        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
86        final RowBuilder row = result.newRow();
87        row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID);
88        row.add(Root.COLUMN_ROOT_TYPE, Root.ROOT_TYPE_SERVICE);
89        row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS);
90        row.add(Root.COLUMN_TITLE, "_Test title which is really long");
91        row.add(Root.COLUMN_SUMMARY, "_Summary which is also super long text");
92        row.add(Root.COLUMN_DOCUMENT_ID, MY_DOC_ID);
93        row.add(Root.COLUMN_AVAILABLE_BYTES, 1024);
94        return result;
95    }
96
97    @Override
98    public Cursor queryDocument(String documentId, String[] projection)
99            throws FileNotFoundException {
100        if (CRASH_DOCUMENT) System.exit(12);
101
102        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
103        includeFile(result, documentId, 0);
104        return result;
105    }
106
107    /**
108     * Holds any outstanding or finished "network" fetching.
109     */
110    private WeakReference<CloudTask> mTask;
111
112    private static class CloudTask implements Runnable {
113
114        private final ContentResolver mResolver;
115        private final Uri mNotifyUri;
116
117        private volatile boolean mFinished;
118
119        public CloudTask(ContentResolver resolver, Uri notifyUri) {
120            mResolver = resolver;
121            mNotifyUri = notifyUri;
122        }
123
124        @Override
125        public void run() {
126            // Pretend to do some network
127            Log.d(TAG, hashCode() + ": pretending to do some network!");
128            SystemClock.sleep(2000);
129            Log.d(TAG, hashCode() + ": network done!");
130
131            mFinished = true;
132
133            // Tell anyone remotely they should requery
134            mResolver.notifyChange(mNotifyUri, null, false);
135        }
136
137        public boolean includeIfFinished(MatrixCursor result) {
138            Log.d(TAG, hashCode() + ": includeIfFinished() found " + mFinished);
139            if (mFinished) {
140                includeFile(result, "_networkfile1", 0);
141                includeFile(result, "_networkfile2", 0);
142                includeFile(result, "_networkfile3", 0);
143                includeFile(result, "_networkfile4", 0);
144                includeFile(result, "_networkfile5", 0);
145                includeFile(result, "_networkfile6", 0);
146                return true;
147            } else {
148                return false;
149            }
150        }
151    }
152
153    private static class CloudCursor extends MatrixCursor {
154        public Object keepAlive;
155        public final Bundle extras = new Bundle();
156
157        public CloudCursor(String[] columnNames) {
158            super(columnNames);
159        }
160
161        @Override
162        public Bundle getExtras() {
163            return extras;
164        }
165    }
166
167    @Override
168    public Cursor queryChildDocuments(
169            String parentDocumentId, String[] projection, String sortOrder)
170            throws FileNotFoundException {
171
172        final ContentResolver resolver = getContext().getContentResolver();
173        final Uri notifyUri = DocumentsContract.buildDocumentUri(
174                "com.example.documents", parentDocumentId);
175
176        CloudCursor result = new CloudCursor(resolveDocumentProjection(projection));
177        result.setNotificationUri(resolver, notifyUri);
178
179        // Always include local results
180        includeFile(result, MY_DOC_NULL, 0);
181        includeFile(result, "localfile1", 0);
182        includeFile(result, "localfile2", Document.FLAG_SUPPORTS_THUMBNAIL);
183        includeFile(result, "localfile3", 0);
184        includeFile(result, "localfile4", 0);
185
186        synchronized (this) {
187            // Try picking up an existing network fetch
188            CloudTask task = mTask != null ? mTask.get() : null;
189            if (task == null) {
190                Log.d(TAG, "No network task found; starting!");
191                task = new CloudTask(resolver, notifyUri);
192                mTask = new WeakReference<CloudTask>(task);
193                new Thread(task).start();
194
195                // Aggressively try freeing weak reference above
196                new Thread() {
197                    @Override
198                    public void run() {
199                        while (mTask.get() != null) {
200                            SystemClock.sleep(200);
201                            System.gc();
202                            System.runFinalization();
203                        }
204                        Log.d(TAG, "AHA! THE CLOUD TASK WAS GC'ED!");
205                    }
206                }.start();
207            }
208
209            // Blend in cloud results if ready
210            if (task.includeIfFinished(result)) {
211                result.extras.putString(DocumentsContract.EXTRA_INFO,
212                        "Everything Went Better Than Expected and this message is quite "
213                                + "long and verbose and maybe even too long");
214                result.extras.putString(DocumentsContract.EXTRA_ERROR,
215                        "But then again, maybe our server ran into an error, which means "
216                                + "we're going to have a bad time");
217            } else {
218                result.extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
219            }
220
221            // Tie the network fetch to the cursor GC lifetime
222            result.keepAlive = task;
223
224            return result;
225        }
226    }
227
228    @Override
229    public Cursor queryRecentDocuments(String rootId, String[] projection)
230            throws FileNotFoundException {
231        // Pretend to take a super long time to respond
232        SystemClock.sleep(3000);
233
234        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
235        includeFile(
236                result, "It was /worth/ the_wait for?the file:with the&incredibly long name", 0);
237        return result;
238    }
239
240    @Override
241    public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
242            throws FileNotFoundException {
243        throw new FileNotFoundException();
244    }
245
246    @Override
247    public AssetFileDescriptor openDocumentThumbnail(
248            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
249        final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
250        final Canvas canvas = new Canvas(bitmap);
251        final Paint paint = new Paint();
252        paint.setColor(Color.BLUE);
253        canvas.drawColor(Color.RED);
254        canvas.drawLine(0, 0, 32, 32, paint);
255
256        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
257        bitmap.compress(CompressFormat.JPEG, 50, bos);
258
259        final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
260        try {
261            final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createReliablePipe();
262            new AsyncTask<Object, Object, Object>() {
263                @Override
264                protected Object doInBackground(Object... params) {
265                    final FileOutputStream fos = new FileOutputStream(fds[1].getFileDescriptor());
266                    try {
267                        Streams.copy(bis, fos);
268                    } catch (IOException e) {
269                        throw new RuntimeException(e);
270                    }
271                    IoUtils.closeQuietly(fds[1]);
272                    return null;
273                }
274            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
275            return new AssetFileDescriptor(fds[0], 0, AssetFileDescriptor.UNKNOWN_LENGTH);
276        } catch (IOException e) {
277            throw new FileNotFoundException(e.getMessage());
278        }
279    }
280
281    @Override
282    public boolean onCreate() {
283        return true;
284    }
285
286    private static void includeFile(MatrixCursor result, String docId, int flags) {
287        final RowBuilder row = result.newRow();
288        row.add(Document.COLUMN_DOCUMENT_ID, docId);
289        row.add(Document.COLUMN_DISPLAY_NAME, docId);
290        row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
291        row.add(Document.COLUMN_FLAGS, flags);
292
293        if (MY_DOC_ID.equals(docId)) {
294            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
295        } else if (MY_DOC_NULL.equals(docId)) {
296            // No MIME type
297        } else {
298            row.add(Document.COLUMN_MIME_TYPE, "application/octet-stream");
299        }
300    }
301}
302