/* * Copyright (C) 2013 uPhyca Inc. http://www.uphyca.com/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.uphyca.kitkat.storage.provider; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import javax.inject.Inject; import android.annotation.TargetApi; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Point; import android.os.AsyncTask; import android.os.Build; import android.os.CancellationSignal; import android.os.Handler; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.provider.DocumentsProvider; import com.uphyca.kitkat.storage.R; import com.uphyca.kitkat.storage.internal.DocumentsColumnMapper; import com.uphyca.kitkat.storage.internal.MimeTypeResolver; import com.uphyca.kitkat.storage.internal.SkyDriveClient; import com.uphyca.kitkat.storage.skydrive.SkyDriveObject; /** * SkyDriveをバックエンドにした DocumentsProvider の実装。 * FIXME アクセスするたびにネットワークアクセスしているので遅い。キャッシュしたり先読みしたりする必要がありそう。 * * @author masui@uphyca.com */ @TargetApi(Build.VERSION_CODES.KITKAT) public class SkyDriveProvider extends DocumentsProvider { private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { DocumentsContract.Root.COLUMN_ROOT_ID, // DocumentsContract.Root.COLUMN_MIME_TYPES, // DocumentsContract.Root.COLUMN_FLAGS, // DocumentsContract.Root.COLUMN_ICON, // DocumentsContract.Root.COLUMN_TITLE, // DocumentsContract.Root.COLUMN_SUMMARY, // DocumentsContract.Root.COLUMN_DOCUMENT_ID, // DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, // }; private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID, // DocumentsContract.Document.COLUMN_MIME_TYPE, // DocumentsContract.Document.COLUMN_DISPLAY_NAME, // DocumentsContract.Document.COLUMN_LAST_MODIFIED, // DocumentsContract.Document.COLUMN_FLAGS, // DocumentsContract.Document.COLUMN_SIZE, // }; @Inject DocumentsColumnMapper mDocumentsColumnMapper; @Inject MimeTypeResolver mMimeTypeResolver; @Inject SkyDriveClient mSkyDriveClient; /** * SkyDriveのルートディレクトリ。 * FIXME プロバイダではなくSkyDriveClientが扱うべき情報 */ private static final String HOME_FOLDER = "me/skydrive"; /** * ルートの情報を返す。最初に必ず呼ばれる。 * * @param projection * @return * @throws FileNotFoundException */ @Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); final MatrixCursor.RowBuilder row = result.newRow(); row.add(DocumentsContract.Root.COLUMN_ROOT_ID, HOME_FOLDER); int flags = 0; flags |= DocumentsContract.Root.FLAG_SUPPORTS_CREATE; //flags |= DocumentsContract.Root.FLAG_SUPPORTS_RECENTS; //flags |= DocumentsContract.Root.FLAG_SUPPORTS_SEARCH; row.add(DocumentsContract.Root.COLUMN_FLAGS, flags); row.add(DocumentsContract.Root.COLUMN_TITLE, getContext().getString(R.string.title)); row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, HOME_FOLDER); row.add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_skydrive); return result; } /** * ドキュメントのメタ情報を取得する為に呼ばれる。 * * @param documentId * @param projection * @return * @throws FileNotFoundException */ @Override public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); if (HOME_FOLDER.equals(documentId)) { includeDefaultDocument(result); return result; } for (SkyDriveObject each : mSkyDriveClient.get(documentId)) { includeFile(result, each); } return result; } /** * ディレクトリ配下のファイルをリストする為に呼ばれる。 * FIXME sortOrderを無視している * FIXME projectionを無視している * * @param parentDocumentId * @param projection * @param sortOrder * @return * @throws FileNotFoundException */ @Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); //SkyDriveはドキュメントのID/filesでファイルをリストする //FIXME このプロバイダがSkyDriveのAPIの詳細を知っているのはよくないので list() メソッドをSkyDriveClientに設けるなどしたほうが良さそう for (SkyDriveObject each : mSkyDriveClient.get(parentDocumentId + "/files")) { includeFile(result, each); } return result; } /** * ファイルの内容を取得する為に呼ばれる。 * * @param documentId * @param mode * @param signal * @return * @throws FileNotFoundException */ @Override public ParcelFileDescriptor openDocument(final String documentId, String mode, final CancellationSignal signal) throws FileNotFoundException { try { final File file = mSkyDriveClient.download(documentId); final int accessMode = ParcelFileDescriptor.parseMode(mode); final boolean isWrite = (mode.indexOf('w') != -1); if (!isWrite) { return ParcelFileDescriptor.open(file, accessMode); } // 書き込みモードで開かれた時は、コールバックを設定する。 // コールバックはクライアントがファイルを編集してクローズした時に呼ばれるので、それをクラウドに同期するトリガーにする。 Handler handler = new Handler(getContext().getMainLooper()); return ParcelFileDescriptor.open(file, accessMode, handler, new ParcelFileDescriptor.OnCloseListener() { @Override public void onClose(IOException e) { // FIXME リトライ処理が要る。 new UploadTask(mSkyDriveClient, documentId, file).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }); } catch (IOException e) { throw new FileNotFoundException("Failed to open document with id " + documentId + " and mode " + mode); } } /** * ファイルを作成する為に呼ばれる。 * * @param parentDocumentId * @param mimeType * @param displayName * @return * @throws FileNotFoundException */ @Override public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { try { return mSkyDriveClient.mkdir(parentDocumentId, displayName); } catch (IOException e) { FileNotFoundException fileNotFound = new FileNotFoundException(e.getMessage()); fileNotFound.initCause(e); throw fileNotFound; } } try { return mSkyDriveClient.touch(parentDocumentId, mMimeTypeResolver.suggestExtensionIfNecessary(mimeType, displayName)); } catch (IOException e) { FileNotFoundException fileNotFound = new FileNotFoundException(e.getMessage()); fileNotFound.initCause(e); throw fileNotFound; } } /** * ファイルを削除する為に呼ばれる。 * * @param documentId * @throws FileNotFoundException */ @Override public void deleteDocument(String documentId) throws FileNotFoundException { // FIXME クライアントからdelete呼ぶとクラッシュする。why? // java.lang.SecurityException: android:deleteDocument: Neither user 10079 nor current process has write permission on content://com.uphyca.kitkat.storage.documents/document/file.1528683a3710d39f.1528683A3710D39F!1109. // at android.app.ContextImpl.enforceForUri(ContextImpl.java:1811) // at android.app.ContextImpl.enforceCallingOrSelfUriPermission(ContextImpl.java:1840) // at android.content.ContextWrapper.enforceCallingOrSelfUriPermission(ContextWrapper.java:622) // at android.provider.DocumentsProvider.call(DocumentsProvider.java:499) // at android.content.ContentProvider$Transport.call(ContentProvider.java:325) // at android.content.ContentProviderClient.call(ContentProviderClient.java:392) // at android.provider.DocumentsContract.deleteDocument(DocumentsContract.java:812) // at android.provider.DocumentsContract.deleteDocument(DocumentsContract.java:796) // at com.uphyca.kitkat.storage.ui.MainFragment$4.doInBackground(MainFragment.java:221) // at com.uphyca.kitkat.storage.ui.MainFragment$4.doInBackground(MainFragment.java:1) // at android.os.AsyncTask$2.call(AsyncTask.java:288) // at java.util.concurrent.FutureTask.run(FutureTask.java:237) // at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231) // at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) // at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) // at java.lang.Thread.run(Thread.java:841) try { mSkyDriveClient.delete(documentId); } catch (IOException e) { FileNotFoundException fileNotFound = new FileNotFoundException(e.getMessage()); fileNotFound.initCause(e); throw fileNotFound; } } /** * ドキュメントの履歴を取得する為に呼ばれる。 * ルートがクエリされた時に、DocumentsContract.Root.FLAG_SUPPORTS_RECENTS を設定していなければ呼ばれない。 * * @param rootId * @param projection * @return * @throws FileNotFoundException */ @Override public Cursor queryRecentDocuments(String rootId, String[] projection) throws FileNotFoundException { //デフォルト実装は例外を投げる return super.queryRecentDocuments(rootId, projection); } /** * ドキュメントを検索する為に呼ばれる。 * ルートがクエリされた時に、DocumentsContract.Root.FLAG_SUPPORTS_SEARCH を設定していなければ呼ばれない。 * * @param rootId * @param query * @param projection * @return * @throws FileNotFoundException */ @Override public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException { //デフォルト実装は例外を投げる return super.querySearchDocuments(rootId, query, projection); } /** * 調べてないので分からない。 * * @param documentId * @return * @throws FileNotFoundException */ @Override public String getDocumentType(String documentId) throws FileNotFoundException { return super.getDocumentType(documentId); } /** * ドキュメントのサムネイルを取得する為に呼ばれる。 * サムネイルを返さないか、例外を投げるとデフォルトのサムネイルが使われる。 * ドキュメントがクエリされた時に、DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL を設定していないドキュメントに対しては呼ばれない。 * * @param documentId * @param sizeHint * @param signal * @return * @throws FileNotFoundException */ @Override public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { return super.openDocumentThumbnail(documentId, sizeHint, signal); } @Override public boolean onCreate() { return true; } private void includeDefaultDocument(MatrixCursor result) { final MatrixCursor.RowBuilder row = result.newRow(); row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, HOME_FOLDER); row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR); int flags = 0; flags |= DocumentsContract.Document.FLAG_DIR_PREFERS_LAST_MODIFIED; flags |= DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE; row.add(DocumentsContract.Document.COLUMN_FLAGS, flags); } private void includeFile(MatrixCursor result, SkyDriveObject skyDriveObj) { final MatrixCursor.RowBuilder row = result.newRow(); row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, mDocumentsColumnMapper.mapDocumentId(skyDriveObj)); row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, mDocumentsColumnMapper.mapMimeType(skyDriveObj)); row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, mDocumentsColumnMapper.mapDisplayName(skyDriveObj)); row.add(DocumentsContract.Document.COLUMN_SUMMARY, mDocumentsColumnMapper.mapSummary(skyDriveObj)); row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, mDocumentsColumnMapper.mapLastModified(skyDriveObj)); row.add(DocumentsContract.Document.COLUMN_ICON, mDocumentsColumnMapper.mapIcon(skyDriveObj)); row.add(DocumentsContract.Document.COLUMN_SIZE, mDocumentsColumnMapper.mapSize(skyDriveObj)); row.add(DocumentsContract.Document.COLUMN_FLAGS, mDocumentsColumnMapper.mapFlags(skyDriveObj)); } private String[] resolveRootProjection(String[] projection) { return projection == null ? DEFAULT_ROOT_PROJECTION : projection; } private String[] resolveDocumentProjection(String[] projection) { return projection == null ? DEFAULT_DOCUMENT_PROJECTION : projection; } private static class UploadTask extends AsyncTask<Void, Void, Void> { private final SkyDriveClient mSkyDriveClient; private final String mDocumentId; private final File mLocalFile; private UploadTask(SkyDriveClient skyDriveClient, String documentId, File localFile) { mSkyDriveClient = skyDriveClient; mDocumentId = documentId; mLocalFile = localFile; } @Override protected Void doInBackground(Void... params) { if (!mLocalFile.exists()) { return null; } final SkyDriveObject[] documents = mSkyDriveClient.get(mDocumentId); if (documents.length < 1) { return null; } SkyDriveObject document = documents[0]; try { mSkyDriveClient.upload(document.getParentId(), document.getName(), mLocalFile); } catch (IOException ignore) { } return null; } } }