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