/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * 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 android.provider;

import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE;
import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT;
import static android.provider.DocumentsContract.getDocumentId;
import static android.provider.DocumentsContract.getRootId;
import static android.provider.DocumentsContract.getSearchDocumentsQuery;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.provider.DocumentsContract.Document;
import android.util.Log;

import libcore.io.IoUtils;

import java.io.FileNotFoundException;

/**
 * Base class for a document provider. A document provider should extend this
 * class and implement the abstract methods.
 * <p>
 * Each document provider expresses one or more "roots" which each serve as the
 * top-level of a tree. For example, a root could represent an account, or a
 * physical storage device. Under each root, documents are referenced by
 * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned.
 * <p>
 * Documents can be either an openable file (with a specific MIME type), or a
 * directory containing additional documents (with the
 * {@link Document#MIME_TYPE_DIR} MIME type). Each document can have different
 * capabilities, as described by {@link Document#COLUMN_FLAGS}. The same
 * {@link Document#COLUMN_DOCUMENT_ID} can be included in multiple directories.
 * <p>
 * Document providers must be protected with the
 * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
 * only be requested by the system. The system-provided UI then issues narrow
 * Uri permission grants for individual documents when the user explicitly picks
 * documents.
 *
 * @see Intent#ACTION_OPEN_DOCUMENT
 * @see Intent#ACTION_CREATE_DOCUMENT
 */
public abstract class DocumentsProvider extends ContentProvider {
    private static final String TAG = "DocumentsProvider";

    private static final int MATCH_ROOTS = 1;
    private static final int MATCH_ROOT = 2;
    private static final int MATCH_RECENT = 3;
    private static final int MATCH_DOCUMENT = 4;
    private static final int MATCH_CHILDREN = 5;
    private static final int MATCH_SEARCH = 6;

    private String mAuthority;

    private UriMatcher mMatcher;

    /**
     * Implementation is provided by the parent class.
     */
    @Override
    public void attachInfo(Context context, ProviderInfo info) {
        mAuthority = info.authority;

        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mMatcher.addURI(mAuthority, "root", MATCH_ROOTS);
        mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT);
        mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT);
        mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
        mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
        mMatcher.addURI(mAuthority, "document/*/search", MATCH_SEARCH);

        // Sanity check our setup
        if (!info.exported) {
            throw new SecurityException("Provider must be exported");
        }
        if (!info.grantUriPermissions) {
            throw new SecurityException("Provider must grantUriPermissions");
        }
        if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
                || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
            throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
        }

        super.attachInfo(context, info);
    }

    /**
     * Create a new document and return its {@link Document#COLUMN_DOCUMENT_ID}.
     * A provider must allocate a new {@link Document#COLUMN_DOCUMENT_ID} to
     * represent the document, which must not change once returned.
     *
     * @param documentId the parent directory to create the new document under.
     * @param mimeType the MIME type associated with the new document.
     * @param displayName the display name of the new document.
     */
    @SuppressWarnings("unused")
    public String createDocument(String documentId, String mimeType, String displayName)
            throws FileNotFoundException {
        throw new UnsupportedOperationException("Create not supported");
    }

    /**
     * Delete the given document. Upon returning, any Uri permission grants for
     * the given document will be revoked. If additional documents were deleted
     * as a side effect of this call, such as documents inside a directory, the
     * implementor is responsible for revoking those permissions.
     *
     * @param documentId the document to delete.
     */
    @SuppressWarnings("unused")
    public void deleteDocument(String documentId) throws FileNotFoundException {
        throw new UnsupportedOperationException("Delete not supported");
    }

    public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException;

    @SuppressWarnings("unused")
    public Cursor queryRecentDocuments(String rootId, String[] projection)
            throws FileNotFoundException {
        throw new UnsupportedOperationException("Recent not supported");
    }

    /**
     * Return metadata for the given document. A provider should avoid making
     * network requests to keep this request fast.
     *
     * @param documentId the document to return.
     */
    public abstract Cursor queryDocument(String documentId, String[] projection)
            throws FileNotFoundException;

    /**
     * Return the children of the given document which is a directory.
     *
     * @param parentDocumentId the directory to return children for.
     */
    public abstract Cursor queryChildDocuments(
            String parentDocumentId, String[] projection, String sortOrder)
            throws FileNotFoundException;

    /**
     * Return documents that that match the given query, starting the search at
     * the given directory.
     *
     * @param parentDocumentId the directory to start search at.
     */
    @SuppressWarnings("unused")
    public Cursor querySearchDocuments(String parentDocumentId, String query, String[] projection)
            throws FileNotFoundException {
        throw new UnsupportedOperationException("Search not supported");
    }

    /**
     * Return MIME type for the given document. Must match the value of
     * {@link Document#COLUMN_MIME_TYPE} for this document.
     */
    public String getDocumentType(String documentId) throws FileNotFoundException {
        final Cursor cursor = queryDocument(documentId, null);
        try {
            if (cursor.moveToFirst()) {
                return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE));
            } else {
                return null;
            }
        } finally {
            IoUtils.closeQuietly(cursor);
        }
    }

    /**
     * Open and return the requested document. A provider should return a
     * reliable {@link ParcelFileDescriptor} to detect when the remote caller
     * has finished reading or writing the document. A provider may return a
     * pipe or socket pair if the mode is exclusively
     * {@link ParcelFileDescriptor#MODE_READ_ONLY} or
     * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like
     * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on
     * disk. If a provider blocks while downloading content, it should
     * periodically check {@link CancellationSignal#isCanceled()} to abort
     * abandoned open requests.
     *
     * @param docId the document to return.
     * @param mode the mode to open with, such as 'r', 'w', or 'rw'.
     * @param signal used by the caller to signal if the request should be
     *            cancelled.
     * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
     *      OnCloseListener)
     * @see ParcelFileDescriptor#createReliablePipe()
     * @see ParcelFileDescriptor#createReliableSocketPair()
     */
    public abstract ParcelFileDescriptor openDocument(
            String docId, String mode, CancellationSignal signal) throws FileNotFoundException;

    /**
     * Open and return a thumbnail of the requested document. A provider should
     * return a thumbnail closely matching the hinted size, attempting to serve
     * from a local cache if possible. A provider should never return images
     * more than double the hinted size. If a provider performs expensive
     * operations to download or generate a thumbnail, it should periodically
     * check {@link CancellationSignal#isCanceled()} to abort abandoned
     * thumbnail requests.
     *
     * @param docId the document to return.
     * @param sizeHint hint of the optimal thumbnail dimensions.
     * @param signal used by the caller to signal if the request should be
     *            cancelled.
     * @see Document#FLAG_SUPPORTS_THUMBNAIL
     */
    @SuppressWarnings("unused")
    public AssetFileDescriptor openDocumentThumbnail(
            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
        throw new UnsupportedOperationException("Thumbnails not supported");
    }

    /**
     * Implementation is provided by the parent class. Cannot be overriden.
     *
     * @see #queryRoots(String[])
     * @see #queryRecentDocuments(String, String[])
     * @see #queryDocument(String, String[])
     * @see #queryChildDocuments(String, String[], String)
     * @see #querySearchDocuments(String, String, String[])
     */
    @Override
    public final Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        try {
            switch (mMatcher.match(uri)) {
                case MATCH_ROOTS:
                    return queryRoots(projection);
                case MATCH_RECENT:
                    return queryRecentDocuments(getRootId(uri), projection);
                case MATCH_DOCUMENT:
                    return queryDocument(getDocumentId(uri), projection);
                case MATCH_CHILDREN:
                    return queryChildDocuments(getDocumentId(uri), projection, sortOrder);
                case MATCH_SEARCH:
                    return querySearchDocuments(
                            getDocumentId(uri), getSearchDocumentsQuery(uri), projection);
                default:
                    throw new UnsupportedOperationException("Unsupported Uri " + uri);
            }
        } catch (FileNotFoundException e) {
            Log.w(TAG, "Failed during query", e);
            return null;
        }
    }

    /**
     * Implementation is provided by the parent class. Cannot be overriden.
     *
     * @see #getDocumentType(String)
     */
    @Override
    public final String getType(Uri uri) {
        try {
            switch (mMatcher.match(uri)) {
                case MATCH_ROOT:
                    return DocumentsContract.Root.MIME_TYPE_ITEM;
                case MATCH_DOCUMENT:
                    return getDocumentType(getDocumentId(uri));
                default:
                    return null;
            }
        } catch (FileNotFoundException e) {
            Log.w(TAG, "Failed during getType", e);
            return null;
        }
    }

    /**
     * Implementation is provided by the parent class. Throws by default, and
     * cannot be overriden.
     *
     * @see #createDocument(String, String, String)
     */
    @Override
    public final Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException("Insert not supported");
    }

    /**
     * Implementation is provided by the parent class. Throws by default, and
     * cannot be overriden.
     *
     * @see #deleteDocument(String)
     */
    @Override
    public final int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Delete not supported");
    }

    /**
     * Implementation is provided by the parent class. Throws by default, and
     * cannot be overriden.
     */
    @Override
    public final int update(
            Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Update not supported");
    }

    /**
     * Implementation is provided by the parent class. Can be overridden to
     * provide additional functionality, but subclasses <em>must</em> always
     * call the superclass. If the superclass returns {@code null}, the subclass
     * may implement custom behavior.
     *
     * @see #openDocument(String, String, CancellationSignal)
     * @see #deleteDocument(String)
     */
    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        final Context context = getContext();

        if (!method.startsWith("android:")) {
            // Let non-platform methods pass through
            return super.call(method, arg, extras);
        }

        final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID);
        final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId);

        // Require that caller can manage given document
        final boolean callerHasManage =
                context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS)
                == PackageManager.PERMISSION_GRANTED;
        if (!callerHasManage) {
            getContext().enforceCallingOrSelfUriPermission(
                    documentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, method);
        }

        final Bundle out = new Bundle();
        try {
            if (METHOD_CREATE_DOCUMENT.equals(method)) {
                final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
                final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);

                final String newDocumentId = createDocument(documentId, mimeType, displayName);
                out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId);

                // Extend permission grant towards caller if needed
                if (!callerHasManage) {
                    final Uri newDocumentUri = DocumentsContract.buildDocumentUri(
                            mAuthority, newDocumentId);
                    context.grantUriPermission(getCallingPackage(), newDocumentUri,
                            Intent.FLAG_GRANT_READ_URI_PERMISSION
                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                            | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
                }

            } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
                deleteDocument(documentId);

                // Document no longer exists, clean up any grants
                context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                        | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);

            } else {
                throw new UnsupportedOperationException("Method not supported " + method);
            }
        } catch (FileNotFoundException e) {
            throw new IllegalStateException("Failed call " + method, e);
        }
        return out;
    }

    /**
     * Implementation is provided by the parent class.
     *
     * @see #openDocument(String, String, CancellationSignal)
     */
    @Override
    public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        return openDocument(getDocumentId(uri), mode, null);
    }

    /**
     * Implementation is provided by the parent class.
     *
     * @see #openDocument(String, String, CancellationSignal)
     */
    @Override
    public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
            throws FileNotFoundException {
        return openDocument(getDocumentId(uri), mode, signal);
    }

    /**
     * Implementation is provided by the parent class.
     *
     * @see #openDocumentThumbnail(String, Point, CancellationSignal)
     */
    @Override
    public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
            throws FileNotFoundException {
        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
            return openDocumentThumbnail(getDocumentId(uri), sizeHint, null);
        } else {
            return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
        }
    }

    /**
     * Implementation is provided by the parent class.
     *
     * @see #openDocumentThumbnail(String, Point, CancellationSignal)
     */
    @Override
    public final AssetFileDescriptor openTypedAssetFile(
            Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
            throws FileNotFoundException {
        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
            return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal);
        } else {
            return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
        }
    }
}