/*
 * Copyright (C) 2016 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.view.inputmethod;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ClipDescription;
import android.content.ContentProvider;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;

import com.android.internal.inputmethod.IInputContentUriToken;

import java.security.InvalidParameterException;

/**
 * A container object with which input methods can send content files to the target application.
 */
public final class InputContentInfo implements Parcelable {

    /**
     * The content URI that may or may not have a user ID embedded by
     * {@link ContentProvider#maybeAddUserId(Uri, int)}.  This always preserves the exact value
     * specified to a constructor.  In other words, if it had user ID embedded when it was passed
     * to the constructor, it still has the same user ID no matter if it is valid or not.
     */
    @NonNull
    private final Uri mContentUri;
    /**
     * The user ID to which {@link #mContentUri} belongs to.  If {@link #mContentUri} already
     * embedded the user ID when it was specified then this fields has the same user ID.  Otherwise
     * the user ID is determined based on the process ID when the constructor is called.
     *
     * <p>CAUTION: If you received {@link InputContentInfo} from a different process, there is no
     * guarantee that this value is correct and valid.  Never use this for any security purpose</p>
     */
    @UserIdInt
    private final int mContentUriOwnerUserId;
    @NonNull
    private final ClipDescription mDescription;
    @Nullable
    private final Uri mLinkUri;
    @NonNull
    private IInputContentUriToken mUriToken;

    /**
     * Constructs {@link InputContentInfo} object only with mandatory data.
     *
     * @param contentUri Content URI to be exported from the input method.
     * This cannot be {@code null}.
     * @param description A {@link ClipDescription} object that contains the metadata of
     * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
     * {@link ClipDescription#getLabel()} should be describing the content specified by
     * {@code contentUri} for accessibility reasons.
     */
    public InputContentInfo(@NonNull Uri contentUri, @NonNull ClipDescription description) {
        this(contentUri, description, null /* link Uri */);
    }

    /**
     * Constructs {@link InputContentInfo} object with additional link URI.
     *
     * @param contentUri Content URI to be exported from the input method.
     * This cannot be {@code null}.
     * @param description A {@link ClipDescription} object that contains the metadata of
     * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
     * {@link ClipDescription#getLabel()} should be describing the content specified by
     * {@code contentUri} for accessibility reasons.
     * @param linkUri An optional {@code http} or {@code https} URI. The editor author may provide
     * a way to navigate the user to the specified web page if this is not {@code null}.
     * @throws InvalidParameterException if any invalid parameter is specified.
     */
    public InputContentInfo(@NonNull Uri contentUri, @NonNull ClipDescription description,
            @Nullable Uri linkUri) {
        validateInternal(contentUri, description, linkUri, true /* throwException */);
        mContentUri = contentUri;
        mContentUriOwnerUserId =
                ContentProvider.getUserIdFromUri(mContentUri, UserHandle.myUserId());
        mDescription = description;
        mLinkUri = linkUri;
    }

    /**
     * @return {@code true} if all the fields are valid.
     * @hide
     */
    public boolean validate() {
        return validateInternal(mContentUri, mDescription, mLinkUri, false /* throwException */);
    }

    /**
     * Constructs {@link InputContentInfo} object with additional link URI.
     *
     * @param contentUri Content URI to be exported from the input method.
     * This cannot be {@code null}.
     * @param description A {@link ClipDescription} object that contains the metadata of
     * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
     * {@link ClipDescription#getLabel()} should be describing the content specified by
     * {@code contentUri} for accessibility reasons.
     * @param linkUri An optional {@code http} or {@code https} URI. The editor author may provide
     * a way to navigate the user to the specified web page if this is not {@code null}.
     * @param throwException {@code true} if this method should throw an
     * {@link InvalidParameterException}.
     * @throws InvalidParameterException if any invalid parameter is specified.
     */
    private static boolean validateInternal(@NonNull Uri contentUri,
            @NonNull ClipDescription description, @Nullable Uri linkUri, boolean throwException) {
        if (contentUri == null) {
            if (throwException) {
                throw new NullPointerException("contentUri");
            }
            return false;
        }
        if (description == null) {
            if (throwException) {
                throw new NullPointerException("description");
            }
            return false;
        }
        final String contentUriScheme = contentUri.getScheme();
        if (!"content".equals(contentUriScheme)) {
            if (throwException) {
                throw new InvalidParameterException("contentUri must have content scheme");
            }
            return false;
        }
        if (linkUri != null) {
            final String scheme = linkUri.getScheme();
            if (scheme == null ||
                    (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
                if (throwException) {
                    throw new InvalidParameterException(
                            "linkUri must have either http or https scheme");
                }
                return false;
            }
        }
        return true;
    }

    /**
     * @return Content URI with which the content can be obtained.
     */
    @NonNull
    public Uri getContentUri() {
        // Fix up the content URI when and only when the caller's user ID does not match the owner's
        // user ID.
        if (mContentUriOwnerUserId != UserHandle.myUserId()) {
            return ContentProvider.maybeAddUserId(mContentUri, mContentUriOwnerUserId);
        }
        return mContentUri;
    }

    /**
     * @return {@link ClipDescription} object that contains the metadata of {@code #getContentUri()}
     * such as MIME type(s). {@link ClipDescription#getLabel()} can be used for accessibility
     * purpose.
     */
    @NonNull
    public ClipDescription getDescription() { return mDescription; }

    /**
     * @return An optional {@code http} or {@code https} URI that is related to this content.
     */
    @Nullable
    public Uri getLinkUri() { return mLinkUri; }

    /**
     * Update the internal state of this object to be associated with the given token.
     *
     * <p>TODO(yukawa): Come up with an idea to make {@link InputContentInfo} immutable.</p>
     *
     * @param token special URI token obtained from the system.
     * @hide
     */
    public void setUriToken(IInputContentUriToken token) {
        if (mUriToken != null) {
            throw new IllegalStateException("URI token is already set");
        }
        mUriToken = token;
    }

    /**
     * Requests a temporary {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION read-only}
     * access permission for the content URI associated with this object.
     *
     * <p>The lifecycle of the permission granted here is tied to this object instance. If the
     * permission is not released explicitly via {@link #releasePermission()}, it will be
     * released automatically when there are no more references to this object.</p>
     *
     * <p>Does nothing if the temporary permission is already granted.</p>
     */
    public void requestPermission() {
        if (mUriToken == null) {
            return;
        }
        try {
            mUriToken.take();
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * Releases a temporary read-only access permission for content URI associated with this object.
     *
     * <p>Does nothing if the temporary permission is not granted.</p>
     */
    public void releasePermission() {
        if (mUriToken == null) {
            return;
        }
        try {
            mUriToken.release();
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * Used to package this object into a {@link Parcel}.
     *
     * @param dest The {@link Parcel} to be written.
     * @param flags The flags used for parceling.
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        Uri.writeToParcel(dest, mContentUri);
        dest.writeInt(mContentUriOwnerUserId);
        mDescription.writeToParcel(dest, flags);
        Uri.writeToParcel(dest, mLinkUri);
        if (mUriToken != null) {
            dest.writeInt(1);
            dest.writeStrongBinder(mUriToken.asBinder());
        } else {
            dest.writeInt(0);
        }
    }

    private InputContentInfo(@NonNull Parcel source) {
        mContentUri = Uri.CREATOR.createFromParcel(source);
        mContentUriOwnerUserId = source.readInt();
        mDescription = ClipDescription.CREATOR.createFromParcel(source);
        mLinkUri = Uri.CREATOR.createFromParcel(source);
        if (source.readInt() == 1) {
            mUriToken = IInputContentUriToken.Stub.asInterface(source.readStrongBinder());
        } else {
            mUriToken = null;
        }
    }

    /**
     * Used to make this class parcelable.
     */
    public static final @android.annotation.NonNull Parcelable.Creator<InputContentInfo> CREATOR
            = new Parcelable.Creator<InputContentInfo>() {
        @Override
        public InputContentInfo createFromParcel(Parcel source) {
            return new InputContentInfo(source);
        }

        @Override
        public InputContentInfo[] newArray(int size) {
            return new InputContentInfo[size];
        }
    };

    /**
     * {@inheritDoc}
     */
    @Override
    public int describeContents() {
        return 0;
    }
}
