com.seafile.seadroid2.provider.SeafileProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.seafile.seadroid2.provider.SeafileProvider.java

Source

/*
 * Copyright (C) 2014 Dariush Forouher
 *
 * Based on the example from https://developer.android.com/samples/StorageProvider/index.html
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.seafile.seadroid2.provider;

import android.accounts.OnAccountsUpdateListener;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.util.Log;

import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.seafile.seadroid2.R;
import com.seafile.seadroid2.SeadroidApplication;
import com.seafile.seadroid2.SeafException;
import com.seafile.seadroid2.account.Account;
import com.seafile.seadroid2.account.AccountManager;
import com.seafile.seadroid2.data.DataManager;
import com.seafile.seadroid2.data.ProgressMonitor;
import com.seafile.seadroid2.data.SeafDirent;
import com.seafile.seadroid2.data.SeafRepo;
import com.seafile.seadroid2.data.SeafStarredFile;
import com.seafile.seadroid2.util.ConcurrentAsyncTask;
import com.seafile.seadroid2.util.Utils;

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * DocumentProvider for the Storage Access Framework.
 *
 * It depends on API level 19 and supports API level 21.
 *
 * This Provider gives access to other Apps to browse, read and write all files
 * contained in Seafile repositories.
 *
 */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class SeafileProvider extends DocumentsProvider {
    public static final String DEBUG_TAG = "SeafileProvider";

    private static final String[] SUPPORTED_ROOT_PROJECTION = new String[] { Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS,
            Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_SUMMARY, Root.COLUMN_ICON };

    private static final String[] SUPPORTED_DOCUMENT_PROJECTION = new String[] { Document.COLUMN_DOCUMENT_ID,
            Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
            Document.COLUMN_FLAGS, Document.COLUMN_SIZE, Document.COLUMN_ICON, Document.COLUMN_SUMMARY };

    /** this flag is used to avoid infinite loops due to background refreshes */
    private boolean returnCachedData = false;

    private DocumentIdParser docIdParser;

    private Set<Account> reachableAccounts = new ConcurrentSkipListSet<Account>();

    private android.accounts.AccountManager androidAccountManager;
    private AccountManager accountManager;

    public static final Uri NOTIFICATION_URI = DocumentsContract.buildRootsUri(Utils.AUTHORITY);

    private OnAccountsUpdateListener accountListener = new OnAccountsUpdateListener() {
        @Override
        public void onAccountsUpdated(android.accounts.Account[] accounts) {
            Context c = SeadroidApplication.getAppContext();
            c.getContentResolver().notifyChange(NOTIFICATION_URI, null);
        }
    };

    @Override
    public boolean onCreate() {
        docIdParser = new DocumentIdParser(getContext());

        accountManager = new AccountManager(getContext());
        androidAccountManager = android.accounts.AccountManager.get(getContext());

        androidAccountManager.addOnAccountsUpdatedListener(accountListener, null, true);

        return true;
    }

    @Override
    public Cursor queryRoots(String[] projection) throws FileNotFoundException {

        String[] netProjection = netProjection(projection, SUPPORTED_ROOT_PROJECTION);
        MatrixCursor result = new MatrixCursor(netProjection);

        Log.d(DEBUG_TAG, "queryRoots()");

        // add a Root for every signed in Seafile account we have.
        for (Account a : accountManager.getAccountList()) {
            includeRoot(result, a);
        }

        // notification uri for the event, that the account list has changed
        result.setNotificationUri(getContext().getContentResolver(), NOTIFICATION_URI);

        return result;
    }

    @Override
    public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
            throws FileNotFoundException {

        Log.d(DEBUG_TAG, "queryChildDocuments: " + parentDocumentId);

        String[] netProjection = netProjection(projection, SUPPORTED_DOCUMENT_PROJECTION);

        DataManager dm = createDataManager(parentDocumentId);

        String repoId = DocumentIdParser.getRepoIdFromId(parentDocumentId);
        String path = DocumentIdParser.getPathFromId(parentDocumentId);

        if (repoId.isEmpty()) {
            // in this case the user is asking for a list of repositories

            MatrixCursor result;

            // fetch a new repo list in the background
            if (!returnCachedData) {
                result = createCursor(netProjection, true, reachableAccounts.contains(dm.getAccount()));
                returnCachedData = true;
                fetchReposAsync(dm, result);
            } else {
                result = createCursor(netProjection, false, reachableAccounts.contains(dm.getAccount()));
                returnCachedData = false;
            }

            // in the meantime, return the cached repos
            includeStarredFilesRepo(result, dm.getAccount());
            List<SeafRepo> repoList = dm.getReposFromCache();
            if (repoList != null) {
                for (SeafRepo repo : repoList) {
                    includeRepo(result, dm.getAccount(), repo);
                }
            }
            return result;

        } else if (DocumentIdParser.isStarredFiles(parentDocumentId)) {
            // the user is asking for the list of starred files

            MatrixCursor result;
            if (!returnCachedData) {
                result = createCursor(netProjection, true, reachableAccounts.contains(dm.getAccount()));
                returnCachedData = true;
                fetchStarredAsync(dm, result);
            } else {
                result = createCursor(netProjection, false, reachableAccounts.contains(dm.getAccount()));
                returnCachedData = false;
            }

            List<SeafStarredFile> starredFiles = dm.getCachedStarredFiles();
            if (starredFiles != null) {
                for (SeafStarredFile d : starredFiles) {
                    includeStarredFileDirent(result, dm, d);
                }
            }
            return result;

        } else {
            // in this case, the repository is known. the user wants the entries of a specific
            // directory in the given repository.

            SeafRepo repo = dm.getCachedRepoByID(repoId);

            // encrypted repos are not supported (we can't ask the user for the passphrase)
            if (repo == null || repo.encrypted) {
                throw new FileNotFoundException();
            }

            MatrixCursor result;

            // fetch new dirents in the background
            if (!returnCachedData) {
                result = createCursor(netProjection, true, reachableAccounts.contains(dm.getAccount()));
                returnCachedData = true;
                fetchDirentAsync(dm, repoId, path, result);
            } else {
                result = createCursor(netProjection, false, reachableAccounts.contains(dm.getAccount()));
                returnCachedData = false;
            }

            // in the meantime return cached ones
            List<SeafDirent> dirents = dm.getCachedDirents(repoId, path);
            if (dirents != null) {
                for (SeafDirent d : dirents) {
                    includeDirent(result, dm, repoId, path, d);
                }

            }
            return result;
        }

    }

    @Override
    public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {

        Log.d(DEBUG_TAG, "queryDocument: " + documentId);

        String[] netProjection = netProjection(projection, SUPPORTED_DOCUMENT_PROJECTION);
        MatrixCursor result = new MatrixCursor(netProjection);

        DataManager dm = createDataManager(documentId);

        String repoId = DocumentIdParser.getRepoIdFromId(documentId);
        if (repoId.isEmpty()) {
            // the user has asked for the base document_id for a root

            includeDocIdRoot(result, dm.getAccount());
            return result;
        }

        // the android API asks us to be quick, so just use the cache.
        SeafRepo repo = dm.getCachedRepoByID(repoId);
        if (repo == null)
            throw new FileNotFoundException();

        String path = DocumentIdParser.getPathFromId(documentId);

        if (docIdParser.isStarredFiles(documentId)) {
            includeStarredFilesRepo(result, dm.getAccount());
        } else if (path.equals(Utils.PATH_SEPERATOR)) {
            // this is the base of the repository. this is special, as we give back the information
            // about the repository itself, not some directory in it.
            includeRepo(result, dm.getAccount(), repo);
        } else {
            // the general case. a query about a file/directory in a repository.

            // again we only use cached info in this function. that shouldn't be an issue, as
            // very likely there has been a SeafileProvider.queryChildDocuments() call just moments
            // earlier.

            String parentPath = Utils.getParentPath(path);
            List<SeafDirent> dirents = dm.getCachedDirents(repo.getID(), parentPath);
            List<SeafStarredFile> starredFiles = dm.getCachedStarredFiles();

            if (dirents != null) {
                // the file is in the dirent of the parent directory

                // look for the requested file in the dirents of the parent dir
                for (SeafDirent entry : dirents) {
                    if (entry.getTitle().equals(Utils.fileNameFromPath(path))) {
                        includeDirent(result, dm, repo.getID(), parentPath, entry);
                    }
                }
            } else if (starredFiles != null) {
                //maybe the requested file is a starred file?

                // look for the requested file in the list of starred files
                for (SeafStarredFile file : starredFiles) {
                    if (file.getPath().equals(path)) {
                        includeStarredFileDirent(result, dm, file);
                    }
                }
            }
        }

        return result;
    }

    @Override
    public boolean isChildDocument(String parentId, String documentId) {
        return documentId.startsWith(parentId);
    }

    @Override
    public ParcelFileDescriptor openDocument(final String documentId, final String mode,
            final CancellationSignal signal) throws FileNotFoundException {

        if (!Utils.isNetworkOn())
            throw new FileNotFoundException();

        // open the file. this might involve talking to the seafile server. this will hang until
        // it is done.
        final Future<ParcelFileDescriptor> future = ConcurrentAsyncTask
                .submit(new Callable<ParcelFileDescriptor>() {

                    @Override
                    public ParcelFileDescriptor call() throws Exception {

                        String path = docIdParser.getPathFromId(documentId);
                        DataManager dm = createDataManager(documentId);
                        String repoId = DocumentIdParser.getRepoIdFromId(documentId);

                        // we can assume that the repo is cached because the client has already seen it
                        SeafRepo repo = dm.getCachedRepoByID(repoId);
                        if (repo == null)
                            throw new FileNotFoundException();

                        File f = getFile(signal, dm, repo, path);

                        // return the file to the client.
                        String parentPath = Utils.getParentPath(path);
                        return makeParcelFileDescriptor(dm, repo.getName(), repoId, parentPath, f, mode);
                    }
                });

        if (signal != null) {
            signal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
                @Override
                public void onCancel() {
                    Log.d(DEBUG_TAG, "openDocument cancelling download");
                    future.cancel(true);
                }
            });
        }

        try {
            return future.get();
        } catch (InterruptedException e) {
            Log.d(DEBUG_TAG, "openDocument cancelled download");
            throw new FileNotFoundException();
        } catch (CancellationException e) {
            Log.d(DEBUG_TAG, "openDocumentThumbnail cancelled download");
            throw new FileNotFoundException();
        } catch (ExecutionException e) {
            Log.d(DEBUG_TAG, "could not open file", e);
            throw new FileNotFoundException();
        }
    }

    @Override
    public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal)
            throws FileNotFoundException {

        Log.d(DEBUG_TAG, "openDocumentThumbnail(): " + documentId);

        String repoId = DocumentIdParser.getRepoIdFromId(documentId);
        if (repoId.isEmpty()) {
            throw new FileNotFoundException();
        }

        String mimeType = Utils.getFileMimeType(documentId);
        if (!mimeType.startsWith("image/")) {
            throw new FileNotFoundException();
        }

        DataManager dm = createDataManager(documentId);

        String path = DocumentIdParser.getPathFromId(documentId);

        final DisplayImageOptions options = new DisplayImageOptions.Builder().extraForDownloader(dm.getAccount())
                .cacheInMemory(false) // SAF does its own caching
                .cacheOnDisk(true).considerExifParams(true).build();

        final ParcelFileDescriptor[] pair;
        try {
            pair = ParcelFileDescriptor.createReliablePipe();
        } catch (IOException e) {
            throw new FileNotFoundException();
        }

        final String url = dm.getThumbnailLink(repoId, path, sizeHint.x);
        if (url == null)
            throw new FileNotFoundException();

        // do thumbnail download in another thread to avoid possible network access in UI thread
        final Future future = ConcurrentAsyncTask.submit(new Runnable() {

            @Override
            public void run() {
                try {
                    FileOutputStream fileStream = new FileOutputStream(pair[1].getFileDescriptor());

                    // load the file. this might involve talking to the seafile server. this will hang until
                    // it is done.
                    Bitmap bmp = ImageLoader.getInstance().loadImageSync(url, options);

                    if (bmp != null) {
                        bmp.compress(Bitmap.CompressFormat.PNG, 100, fileStream);
                    }
                } finally {
                    IOUtils.closeQuietly(pair[1]);
                }
            }
        });

        if (signal != null) {
            signal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
                @Override
                public void onCancel() {
                    Log.d(DEBUG_TAG, "openDocumentThumbnail() cancelling download");
                    future.cancel(true);
                    IOUtils.closeQuietly(pair[1]);
                }
            });
        }

        return new AssetFileDescriptor(pair[0], 0, AssetFileDescriptor.UNKNOWN_LENGTH);
    }

    @Override
    public String createDocument(String parentDocumentId, String mimeType, String displayName)
            throws FileNotFoundException {
        Log.d(DEBUG_TAG, "createDocument: " + parentDocumentId + "; " + mimeType + "; " + displayName);

        if (!Utils.isNetworkOn())
            throw new FileNotFoundException();

        String repoId = DocumentIdParser.getRepoIdFromId(parentDocumentId);
        if (repoId.isEmpty()) {
            throw new FileNotFoundException();
        }

        String parentPath = DocumentIdParser.getPathFromId(parentDocumentId);
        DataManager dm = createDataManager(parentDocumentId);

        try {

            dm.getReposFromServer(); // refresh cache
            SeafRepo repo = dm.getCachedRepoByID(repoId);

            List<SeafDirent> list = dm.getDirentsFromServer(repoId, parentPath);
            if (list == null) {
                throw new SeafException(0,
                        SeadroidApplication.getAppContext().getString(R.string.saf_write_diretory_exception));
            }

            // first check if target already exist. if yes, abort
            for (SeafDirent e : list) {
                if (e.getTitle().equals(displayName)) {
                    throw new SeafException(0,
                            SeadroidApplication.getAppContext().getString(R.string.saf_file_exist));
                }
            }

            if (repo == null || !repo.hasWritePermission()) {
                throw new SeafException(0,
                        SeadroidApplication.getAppContext().getString(R.string.saf_write_diretory_exception));
            } else if (mimeType == null) {
                // bad mime type given by caller
                throw new SeafException(0,
                        SeadroidApplication.getAppContext().getString(R.string.saf_bad_mime_type));
            } else if (mimeType.equals(Document.MIME_TYPE_DIR)) {
                dm.createNewDir(repoId, parentPath, displayName);
            } else {
                dm.createNewFile(repoId, parentPath, displayName);
            }

            // update parent dirent cache
            dm.getDirentsFromServer(repoId, parentPath);

            return DocumentIdParser.buildId(dm.getAccount(), repoId, Utils.pathJoin(parentPath, displayName));

        } catch (SeafException e) {
            Log.d(DEBUG_TAG, "could not create file/dir", e);
            throw new FileNotFoundException();
        }
    }

    /**
     * Create a MatrixCursor with the option to enable the extraLoading flag.
     *
     * @param netProjection column list
     * @param extraLoading if true, the client will expect that more entries will arrive shortly.
     * @return the Cursor object
     */
    private static MatrixCursor createCursor(String[] netProjection, final boolean extraLoading,
            final boolean isReachable) {
        return new MatrixCursor(netProjection) {
            @Override
            public Bundle getExtras() {
                Bundle b = new Bundle();
                b.putBoolean(DocumentsContract.EXTRA_LOADING, extraLoading);
                if (!extraLoading && !isReachable) {
                    b.putString(DocumentsContract.EXTRA_ERROR, "Could not connect with server");
                }
                return b;
            }
        };
    }

    /**
     * Create ParcelFileDescriptor from the given file.
     *
     * @param file the file
     * @param mode the mode the file shoall be opened with.
     * @return a ParcelFileDescriptor
     * @throws IOException
     */
    private ParcelFileDescriptor makeParcelFileDescriptor(final DataManager dm, final String repoName,
            final String repoID, final String parentDir, final File file, final String mode) throws IOException {
        final int accessMode = ParcelFileDescriptor.parseMode(mode);

        Handler handler = new Handler(getContext().getMainLooper());

        return ParcelFileDescriptor.open(file, accessMode, handler, new ParcelFileDescriptor.OnCloseListener() {
            @Override
            public void onClose(final IOException e) {
                Log.d(DEBUG_TAG,
                        "uploading file: " + repoID + "; " + file.getPath() + "; " + parentDir + "; e=" + e);

                if (mode.equals("r") || e != null) {
                    return;
                }

                ConcurrentAsyncTask.submit(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            dm.uploadFile(repoName, repoID, parentDir, file.getPath(), null, true, false);

                            // update cache for parent dir
                            dm.getDirentsFromServer(repoID, parentDir);
                        } catch (SeafException e1) {
                            Log.d(DEBUG_TAG, "could not upload file: ", e1);
                        }
                    }
                });
            }

        });
    }

    /**
     * Load a file from the Seafile server.
     *
     * This might take a while, therefore we have to listen to the CancellationSignal and abort
     * if it says so.
     *
     * @param signal CancellationSignal
     * @param dm DataManager object belonging to the seafile account, where the file lies
     * @param repo The repository where the file lies
     * @param path File path
     * @return
     * @throws FileNotFoundException
     */
    private static File getFile(final CancellationSignal signal, DataManager dm, SeafRepo repo, String path)
            throws FileNotFoundException {

        try {
            // fetch the file from the Seafile server.
            File f = dm.getFile(repo.getName(), repo.getID(), path, new ProgressMonitor() {
                @Override
                public void onProgressNotify(long total, boolean updateTotal) {
                }

                @Override
                public boolean isCancelled() {
                    // cancel the download if the client has lost interest.
                    if (signal != null)
                        return signal.isCanceled();
                    else
                        return false;
                }
            });

            if (f == null) {
                throw new FileNotFoundException();
            }

            if (f.isDirectory()) {
                throw new FileNotFoundException();
            }

            return f;

        } catch (SeafException e) {
            throw new FileNotFoundException();
        }
    }

    /**
     * Add a cursor entry for the account root.
     *
     * We don't know much about it.
     *
     * @param result the cursor to write the row into.
     * @param account the account to add.
     */
    private void includeRoot(MatrixCursor result, Account account) {
        String docId = DocumentIdParser.buildId(account, null, null);
        String rootId = DocumentIdParser.buildRootId(account);

        final MatrixCursor.RowBuilder row = result.newRow();

        row.add(Root.COLUMN_ROOT_ID, rootId);
        row.add(Root.COLUMN_DOCUMENT_ID, docId);
        row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
        row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
        row.add(Root.COLUMN_TITLE, account.getServerHost());
        row.add(Root.COLUMN_SUMMARY, account.getEmail());
    }

    /**
     * Add a cursor entry for the account base document_id.
     *
     * @param result the cursor to write the row into.
     * @param account the account to add.
     */
    private void includeDocIdRoot(MatrixCursor result, Account account) {
        String docId = DocumentIdParser.buildId(account, null, null);

        final MatrixCursor.RowBuilder row = result.newRow();
        row.add(Document.COLUMN_DOCUMENT_ID, docId);
        row.add(Document.COLUMN_DISPLAY_NAME, account.getServerHost());
        row.add(Document.COLUMN_LAST_MODIFIED, null);
        row.add(Document.COLUMN_FLAGS, 0);
        row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
        row.add(Document.COLUMN_SIZE, null);
        row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    }

    /**
     * Add a seafile repo to the cursor.
     *
     * @param result the cursor to write the row into.
     * @param account the account that contains the repo.
     * @param repo the repo to add.
     */
    private void includeRepo(MatrixCursor result, Account account, SeafRepo repo) {
        String docId = DocumentIdParser.buildId(account, repo.getID(), null);

        int flags = 0;
        if (repo.hasWritePermission()) {
            flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
        }

        final MatrixCursor.RowBuilder row = result.newRow();
        row.add(Document.COLUMN_DOCUMENT_ID, docId);
        row.add(Document.COLUMN_DISPLAY_NAME, repo.getTitle());
        row.add(Document.COLUMN_SUMMARY, null);
        row.add(Document.COLUMN_LAST_MODIFIED, repo.mtime * 1000);
        row.add(Document.COLUMN_FLAGS, flags);
        row.add(Document.COLUMN_ICON, repo.getIcon());
        row.add(Document.COLUMN_SIZE, repo.size);

        if (repo.encrypted || !reachableAccounts.contains(account)) {
            row.add(Document.COLUMN_MIME_TYPE, null); // undocumented: will grey out the entry
        } else {
            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
        }
    }

    private void includeStarredFilesRepo(MatrixCursor result, Account account) {
        String docId = DocumentIdParser.buildStarredFilesId(account);

        final MatrixCursor.RowBuilder row = result.newRow();
        row.add(Document.COLUMN_DOCUMENT_ID, docId);

        row.add(Document.COLUMN_DISPLAY_NAME,
                SeadroidApplication.getAppContext().getResources().getString(R.string.tabs_starred));
        row.add(Document.COLUMN_ICON, R.drawable.star_normal);
        row.add(Document.COLUMN_FLAGS, 0);

        if (!reachableAccounts.contains(account)) {
            row.add(Document.COLUMN_MIME_TYPE, null); // undocumented: will grey out the entry
        } else {
            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
        }

    }

    /**
     * add a dirent to the cursor.
     *
     * @param result the cursor to write the row into.
     * @param dm the dataMamager that belongs to the repo.
     * @param repoId the repoId of the seafile repo
     * @param parentPath the path of the parent directory
     * @param entry the seafile dirent to add
     */
    private void includeDirent(MatrixCursor result, DataManager dm, String repoId, String parentPath,
            SeafDirent entry) {
        String fullPath = Utils.pathJoin(parentPath, entry.getTitle());

        String docId = DocumentIdParser.buildId(dm.getAccount(), repoId, fullPath);

        String mimeType;
        if (entry.isDir())
            mimeType = DocumentsContract.Document.MIME_TYPE_DIR;
        else
            mimeType = Utils.getFileMimeType(docId);

        int flags = 0;
        // only offer a thumbnail if the file is an image
        if (mimeType.startsWith("image/")) {
            flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
        }

        SeafRepo repo = dm.getCachedRepoByID(repoId);
        if (repo != null && repo.hasWritePermission()) {
            if (entry.isDir()) {
                flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
            } else {
                flags |= Document.FLAG_SUPPORTS_WRITE;
            }
        }

        final MatrixCursor.RowBuilder row = result.newRow();
        row.add(Document.COLUMN_DOCUMENT_ID, docId);
        row.add(Document.COLUMN_DISPLAY_NAME, entry.getTitle());
        row.add(Document.COLUMN_SIZE, entry.size);
        row.add(Document.COLUMN_SUMMARY, null);
        row.add(Document.COLUMN_LAST_MODIFIED, entry.mtime * 1000);
        row.add(Document.COLUMN_FLAGS, flags);

        if (!reachableAccounts.contains(dm.getAccount())) {
            row.add(Document.COLUMN_MIME_TYPE, null); // undocumented: will grey out the entry
        } else {
            row.add(Document.COLUMN_MIME_TYPE, mimeType);
        }

    }

    /**
     * add a dirent to the cursor.
     *
     * @param result the cursor to write the row into.
     * @param dm the dataMamager that belongs to the repo.
     * @param entry the seafile dirent to add
     */
    private void includeStarredFileDirent(MatrixCursor result, DataManager dm, SeafStarredFile entry) {
        String docId = DocumentIdParser.buildId(dm.getAccount(), entry.getRepoID(), entry.getPath());

        String mimeType;
        if (entry.isDir())
            mimeType = DocumentsContract.Document.MIME_TYPE_DIR;
        else
            mimeType = Utils.getFileMimeType(docId);

        int flags = 0;
        // only offer a thumbnail if the file is an image
        if (mimeType.startsWith("image/")) {
            flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
        }

        final MatrixCursor.RowBuilder row = result.newRow();
        row.add(Document.COLUMN_DOCUMENT_ID, docId);
        row.add(Document.COLUMN_DISPLAY_NAME, entry.getTitle());
        row.add(Document.COLUMN_SIZE, entry.getSize());
        row.add(Document.COLUMN_SUMMARY, null);
        row.add(Document.COLUMN_LAST_MODIFIED, entry.getMtime() * 1000);
        row.add(Document.COLUMN_FLAGS, flags);

        if (!reachableAccounts.contains(dm.getAccount())) {
            row.add(Document.COLUMN_MIME_TYPE, null); // undocumented: will grey out the entry
        } else {
            row.add(Document.COLUMN_MIME_TYPE, mimeType);
        }

    }

    /**
     * Fetches a dirent (list of entries of a directory) from Seafile asynchronously.
     *
     * This will return nothing. It will only signal the client over the MatrixCursor. The client
     * will then recall DocumentProvider.queryChildDocuments() again.
     *
     * @param dm the dataManager to be used.
     * @param repoId the Id of the repository
     * @param path the path of the directory.
     * @param result Cursor object over which to signal the client.
     */
    private void fetchDirentAsync(final DataManager dm, final String repoId, final String path,
            MatrixCursor result) {
        final Uri uri = DocumentsContract.buildChildDocumentsUri(Utils.AUTHORITY,
                docIdParser.buildId(dm.getAccount(), repoId, path));
        result.setNotificationUri(getContext().getContentResolver(), uri);

        ConcurrentAsyncTask.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // fetch the dirents from the server
                    dm.getDirentsFromServer(repoId, path);
                    reachableAccounts.add(dm.getAccount());

                } catch (SeafException e) {
                    Log.e(DEBUG_TAG, "Exception while querying dirents", e);
                    reachableAccounts.remove(dm.getAccount());
                }

                // The notification has to be sent only *after* queryChildDocuments has
                // finished. To be safe, wait a bit.
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e1) {
                }

                // notify the SAF to to do a new queryChildDocuments
                getContext().getContentResolver().notifyChange(uri, null);
            }
        });
    }

    /**
     * Fetches starred files from Seafile asynchronously.
     *
     * This will return nothing. It will only signal the client over the MatrixCursor. The client
     * will then recall DocumentProvider.queryChildDocuments() again.
     *
     * @param dm the dataManager to be used.
     * @param result Cursor object over which to signal the client.
     */
    private void fetchStarredAsync(final DataManager dm, MatrixCursor result) {
        final Uri uri = DocumentsContract.buildChildDocumentsUri(Utils.AUTHORITY,
                docIdParser.buildStarredFilesId(dm.getAccount()));
        result.setNotificationUri(getContext().getContentResolver(), uri);

        ConcurrentAsyncTask.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    dm.getStarredFiles();
                    reachableAccounts.add(dm.getAccount());

                } catch (SeafException e) {
                    Log.e(DEBUG_TAG, "Exception while querying starred files", e);
                    reachableAccounts.remove(dm.getAccount());
                }

                // The notification has to be sent only *after* queryChildDocuments has
                // finished. To be safe, wait a bit.
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e1) {
                }

                // notify the SAF to to do a new queryChildDocuments
                getContext().getContentResolver().notifyChange(uri, null);
            }
        });
    }

    /**
     * Fetches a new list of repositories from Seafile asynchronously.
     *
     * This will return nothing. It will only signal the client over the MatrixCursor. The client
     * will then recall DocumentProvider.queryChildDocuments() again.
     *
     * @param dm the dataManager to be used.
     * @param result Cursor object over which to signal the client.
     */
    private void fetchReposAsync(final DataManager dm, MatrixCursor result) {
        final Uri uri = DocumentsContract.buildChildDocumentsUri(Utils.AUTHORITY,
                docIdParser.buildId(dm.getAccount(), null, null));
        result.setNotificationUri(getContext().getContentResolver(), uri);

        ConcurrentAsyncTask.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // fetch new repositories from the server
                    dm.getReposFromServer();
                    reachableAccounts.add(dm.getAccount());

                } catch (SeafException e) {
                    Log.e(DEBUG_TAG, "Exception while querying repos", e);
                    reachableAccounts.remove(dm.getAccount());
                }

                // The notification has to be sent only *after* queryChildDocuments has
                // finished. To be safe, wait a bit.
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e1) {
                }

                // notify the SAF to to do a new queryChildDocuments
                getContext().getContentResolver().notifyChange(uri, null);
            }
        });
    }

    /**
     * Create a new DataManager (which gives us access to the Seafile cache and server).
     *
     * @param documentId documentId, must contain at least the account
     * @return dataManager object.
     * @throws FileNotFoundException if documentId is bogus or the account does not exist.
     */
    private DataManager createDataManager(String documentId) throws FileNotFoundException {

        Account account = docIdParser.getAccountFromId(documentId);

        return new DataManager(account);
    }

    /**
     * Reduce column list to what we support.
     *
     * @param requested requested columns
     * @param supported supported columns
     * @return common elements of both.
     */
    private static String[] netProjection(String[] requested, String[] supported) {
        if (requested == null) {
            return (supported);
        }

        ArrayList<String> result = new ArrayList<String>();

        for (String request : requested) {
            for (String support : supported) {
                if (request.equals(support)) {
                    result.add(request);
                    break;
                }
            }
        }

        return (result.toArray(new String[0]));
    }

}