Java tutorial
/* * 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])); } }