Home | History | Annotate | Download | only in externalstorage
      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 
     17 package com.android.externalstorage;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.Context;
     21 import android.content.pm.ProviderInfo;
     22 import android.content.res.AssetFileDescriptor;
     23 import android.database.Cursor;
     24 import android.database.MatrixCursor;
     25 import android.database.MatrixCursor.RowBuilder;
     26 import android.graphics.Bitmap;
     27 import android.graphics.Bitmap.CompressFormat;
     28 import android.graphics.Canvas;
     29 import android.graphics.Color;
     30 import android.graphics.Paint;
     31 import android.graphics.Point;
     32 import android.net.Uri;
     33 import android.os.AsyncTask;
     34 import android.os.Bundle;
     35 import android.os.CancellationSignal;
     36 import android.os.CancellationSignal.OnCancelListener;
     37 import android.os.ParcelFileDescriptor;
     38 import android.os.SystemClock;
     39 import android.provider.DocumentsContract;
     40 import android.provider.DocumentsContract.Document;
     41 import android.provider.DocumentsContract.Root;
     42 import android.provider.DocumentsProvider;
     43 import android.util.Log;
     44 
     45 import libcore.io.IoUtils;
     46 import libcore.io.Streams;
     47 
     48 import java.io.ByteArrayInputStream;
     49 import java.io.ByteArrayOutputStream;
     50 import java.io.FileNotFoundException;
     51 import java.io.FileOutputStream;
     52 import java.io.IOException;
     53 import java.lang.ref.WeakReference;
     54 
     55 public 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