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.Context;
21import android.content.pm.ProviderInfo;
22import android.content.res.AssetFileDescriptor;
23import android.database.Cursor;
24import android.database.MatrixCursor;
25import android.database.MatrixCursor.RowBuilder;
26import android.graphics.Bitmap;
27import android.graphics.Bitmap.CompressFormat;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Paint;
31import android.graphics.Point;
32import android.net.Uri;
33import android.os.AsyncTask;
34import android.os.Bundle;
35import android.os.CancellationSignal;
36import android.os.CancellationSignal.OnCancelListener;
37import android.os.ParcelFileDescriptor;
38import android.os.SystemClock;
39import android.provider.DocumentsContract;
40import android.provider.DocumentsContract.Document;
41import android.provider.DocumentsContract.Root;
42import android.provider.DocumentsProvider;
43import android.util.Log;
44
45import libcore.io.IoUtils;
46import libcore.io.Streams;
47
48import java.io.ByteArrayInputStream;
49import java.io.ByteArrayOutputStream;
50import java.io.FileNotFoundException;
51import java.io.FileOutputStream;
52import java.io.IOException;
53import java.lang.ref.WeakReference;
54
55public class TestDocumentsProvider extends DocumentsProvider {
56    private static final String TAG = "TestDocuments";
57
58    private static final boolean LAG = false;
59
60    private static final boolean ROOT_LAME_PROJECTION = false;
61    private static final boolean DOCUMENT_LAME_PROJECTION = false;
62
63    private static final boolean ROOTS_WEDGE = false;
64    private static final boolean ROOTS_CRASH = false;
65    private static final boolean ROOTS_REFRESH = false;
66
67    private static final boolean DOCUMENT_CRASH = false;
68
69    private static final boolean RECENT_WEDGE = false;
70
71    private static final boolean CHILD_WEDGE = false;
72    private static final boolean CHILD_CRASH = false;
73
74    private static final boolean THUMB_HUNDREDS = false;
75    private static final boolean THUMB_WEDGE = false;
76    private static final boolean THUMB_CRASH = false;
77
78    private static final String MY_ROOT_ID = "myRoot";
79    private static final String MY_DOC_ID = "myDoc";
80    private static final String MY_DOC_NULL = "myNull";
81
82    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
83            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
84            Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
85            Root.COLUMN_AVAILABLE_BYTES,
86    };
87
88    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
89            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
90            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
91    };
92
93    private static String[] resolveRootProjection(String[] projection) {
94        if (ROOT_LAME_PROJECTION) return new String[0];
95        return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
96    }
97
98    private static String[] resolveDocumentProjection(String[] projection) {
99        if (DOCUMENT_LAME_PROJECTION) return new String[0];
100        return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
101    }
102
103    private String mAuthority;
104
105    @Override
106    public void attachInfo(Context context, ProviderInfo info) {
107        mAuthority = info.authority;
108        super.attachInfo(context, info);
109    }
110
111    @Override
112    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
113        Log.d(TAG, "Someone asked for our roots!");
114
115        if (LAG) lagUntilCanceled(null);
116        if (ROOTS_WEDGE) wedgeUntilCanceled(null);
117        if (ROOTS_CRASH) System.exit(12);
118
119        if (ROOTS_REFRESH) {
120            new AsyncTask<Void, Void, Void>() {
121                @Override
122                protected Void doInBackground(Void... params) {
123                    SystemClock.sleep(3000);
124                    Log.d(TAG, "Notifying that something changed!!");
125                    final Uri uri = DocumentsContract.buildRootsUri(mAuthority);
126                    getContext().getContentResolver().notifyChange(uri, null, false);
127                    return null;
128                }
129            }.execute();
130        }
131
132        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
133        final RowBuilder row = result.newRow();
134        row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID);
135        row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_CREATE);
136        row.add(Root.COLUMN_TITLE, "_Test title which is really long");
137        row.add(Root.COLUMN_SUMMARY,
138                SystemClock.elapsedRealtime() + " summary which is also super long text");
139        row.add(Root.COLUMN_DOCUMENT_ID, MY_DOC_ID);
140        row.add(Root.COLUMN_AVAILABLE_BYTES, 1024);
141        return result;
142    }
143
144    @Override
145    public Cursor queryDocument(String documentId, String[] projection)
146            throws FileNotFoundException {
147        if (LAG) lagUntilCanceled(null);
148        if (DOCUMENT_CRASH) System.exit(12);
149
150        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
151        includeFile(result, documentId, 0);
152        return result;
153    }
154
155    @Override
156    public String createDocument(String parentDocumentId, String mimeType, String displayName)
157            throws FileNotFoundException {
158        if (LAG) lagUntilCanceled(null);
159
160        return super.createDocument(parentDocumentId, mimeType, displayName);
161    }
162
163    /**
164     * Holds any outstanding or finished "network" fetching.
165     */
166    private WeakReference<CloudTask> mTask;
167
168    private static class CloudTask implements Runnable {
169
170        private final ContentResolver mResolver;
171        private final Uri mNotifyUri;
172
173        private volatile boolean mFinished;
174
175        public CloudTask(ContentResolver resolver, Uri notifyUri) {
176            mResolver = resolver;
177            mNotifyUri = notifyUri;
178        }
179
180        @Override
181        public void run() {
182            // Pretend to do some network
183            Log.d(TAG, hashCode() + ": pretending to do some network!");
184            SystemClock.sleep(2000);
185            Log.d(TAG, hashCode() + ": network done!");
186
187            mFinished = true;
188
189            // Tell anyone remotely they should requery
190            mResolver.notifyChange(mNotifyUri, null, false);
191        }
192
193        public boolean includeIfFinished(MatrixCursor result) {
194            Log.d(TAG, hashCode() + ": includeIfFinished() found " + mFinished);
195            if (mFinished) {
196                includeFile(result, "_networkfile1", 0);
197                includeFile(result, "_networkfile2", 0);
198                includeFile(result, "_networkfile3", 0);
199                includeFile(result, "_networkfile4", 0);
200                includeFile(result, "_networkfile5", 0);
201                includeFile(result, "_networkfile6", 0);
202                return true;
203            } else {
204                return false;
205            }
206        }
207    }
208
209    private static class CloudCursor extends MatrixCursor {
210        public Object keepAlive;
211        public final Bundle extras = new Bundle();
212
213        public CloudCursor(String[] columnNames) {
214            super(columnNames);
215        }
216
217        @Override
218        public Bundle getExtras() {
219            return extras;
220        }
221    }
222
223    @Override
224    public Cursor queryChildDocuments(
225            String parentDocumentId, String[] projection, String sortOrder)
226            throws FileNotFoundException {
227
228        if (LAG) lagUntilCanceled(null);
229        if (CHILD_WEDGE) SystemClock.sleep(Integer.MAX_VALUE);
230        if (CHILD_CRASH) System.exit(12);
231
232        final ContentResolver resolver = getContext().getContentResolver();
233        final Uri notifyUri = DocumentsContract.buildDocumentUri(
234                "com.example.documents", parentDocumentId);
235
236        CloudCursor result = new CloudCursor(resolveDocumentProjection(projection));
237        result.setNotificationUri(resolver, notifyUri);
238
239        // Always include local results
240        includeFile(result, MY_DOC_NULL, 0);
241        includeFile(result, "localfile1", 0);
242        includeFile(result, "localfile2", Document.FLAG_SUPPORTS_THUMBNAIL);
243        includeFile(result, "localfile3", 0);
244        includeFile(result, "localfile4", 0);
245
246        if (THUMB_HUNDREDS) {
247            for (int i = 0; i < 256; i++) {
248                includeFile(result, "i maded u an picshure" + i, Document.FLAG_SUPPORTS_THUMBNAIL);
249            }
250        }
251
252        synchronized (this) {
253            // Try picking up an existing network fetch
254            CloudTask task = mTask != null ? mTask.get() : null;
255            if (task == null) {
256                Log.d(TAG, "No network task found; starting!");
257                task = new CloudTask(resolver, notifyUri);
258                mTask = new WeakReference<CloudTask>(task);
259                new Thread(task).start();
260
261                // Aggressively try freeing weak reference above
262                new Thread() {
263                    @Override
264                    public void run() {
265                        while (mTask.get() != null) {
266                            SystemClock.sleep(200);
267                            System.gc();
268                            System.runFinalization();
269                        }
270                        Log.d(TAG, "AHA! THE CLOUD TASK WAS GC'ED!");
271                    }
272                }.start();
273            }
274
275            // Blend in cloud results if ready
276            if (task.includeIfFinished(result)) {
277                result.extras.putString(DocumentsContract.EXTRA_INFO,
278                        "Everything Went Better Than Expected and this message is quite "
279                                + "long and verbose and maybe even too long");
280                result.extras.putString(DocumentsContract.EXTRA_ERROR,
281                        "But then again, maybe our server ran into an error, which means "
282                                + "we're going to have a bad time");
283            } else {
284                result.extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
285            }
286
287            // Tie the network fetch to the cursor GC lifetime
288            result.keepAlive = task;
289
290            return result;
291        }
292    }
293
294    @Override
295    public Cursor queryRecentDocuments(String rootId, String[] projection)
296            throws FileNotFoundException {
297
298        if (LAG) lagUntilCanceled(null);
299        if (RECENT_WEDGE) wedgeUntilCanceled(null);
300
301        // Pretend to take a super long time to respond
302        SystemClock.sleep(3000);
303
304        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
305        includeFile(
306                result, "It was /worth/ the_wait for?the file:with the&incredibly long name", 0);
307        return result;
308    }
309
310    @Override
311    public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
312            throws FileNotFoundException {
313        if (LAG) lagUntilCanceled(null);
314        throw new FileNotFoundException();
315    }
316
317    @Override
318    public AssetFileDescriptor openDocumentThumbnail(
319            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
320
321        if (LAG) lagUntilCanceled(signal);
322        if (THUMB_WEDGE) wedgeUntilCanceled(signal);
323        if (THUMB_CRASH) System.exit(12);
324
325        final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
326        final Canvas canvas = new Canvas(bitmap);
327        final Paint paint = new Paint();
328        paint.setColor(Color.BLUE);
329        canvas.drawColor(Color.RED);
330        canvas.drawLine(0, 0, 32, 32, paint);
331
332        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
333        bitmap.compress(CompressFormat.JPEG, 50, bos);
334
335        final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
336        try {
337            final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createReliablePipe();
338            new AsyncTask<Object, Object, Object>() {
339                @Override
340                protected Object doInBackground(Object... params) {
341                    final FileOutputStream fos = new FileOutputStream(fds[1].getFileDescriptor());
342                    try {
343                        Streams.copy(bis, fos);
344                    } catch (IOException e) {
345                        throw new RuntimeException(e);
346                    }
347                    IoUtils.closeQuietly(fds[1]);
348                    return null;
349                }
350            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
351            return new AssetFileDescriptor(fds[0], 0, AssetFileDescriptor.UNKNOWN_LENGTH);
352        } catch (IOException e) {
353            throw new FileNotFoundException(e.getMessage());
354        }
355    }
356
357    @Override
358    public boolean onCreate() {
359        return true;
360    }
361
362    private static void lagUntilCanceled(CancellationSignal signal) {
363        waitForCancelOrTimeout(signal, 1500);
364    }
365
366    private static void wedgeUntilCanceled(CancellationSignal signal) {
367        waitForCancelOrTimeout(signal, Integer.MAX_VALUE);
368    }
369
370    private static void waitForCancelOrTimeout(
371            final CancellationSignal signal, long timeoutMillis) {
372        if (signal != null) {
373            final Thread blocked = Thread.currentThread();
374            signal.setOnCancelListener(new OnCancelListener() {
375                @Override
376                public void onCancel() {
377                    blocked.interrupt();
378                }
379            });
380            signal.throwIfCanceled();
381        }
382
383        try {
384            Thread.sleep(timeoutMillis);
385        } catch (InterruptedException e) {
386        }
387
388        if (signal != null) {
389            signal.throwIfCanceled();
390        }
391    }
392
393    private static void includeFile(MatrixCursor result, String docId, int flags) {
394        final RowBuilder row = result.newRow();
395        row.add(Document.COLUMN_DOCUMENT_ID, docId);
396        row.add(Document.COLUMN_DISPLAY_NAME, docId);
397        row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
398        row.add(Document.COLUMN_FLAGS, flags);
399
400        if (MY_DOC_ID.equals(docId)) {
401            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
402            row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_SUPPORTS_CREATE);
403        } else if (MY_DOC_NULL.equals(docId)) {
404            // No MIME type
405        } else {
406            row.add(Document.COLUMN_MIME_TYPE, "application/octet-stream");
407        }
408    }
409}
410