/*
 * Copyright (C) 2007 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 com.android.internal.inputmethod;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Matrix;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.InputChannel;

import com.android.internal.view.IInputMethodSession;

import java.lang.annotation.Retention;

/**
 * Bundle of information returned by input method manager about a successful
 * binding to an input method.
 */
public final class InputBindResult implements Parcelable {

    @Retention(SOURCE)
    @IntDef({
            ResultCode.SUCCESS_WITH_IME_SESSION,
            ResultCode.SUCCESS_WAITING_IME_SESSION,
            ResultCode.SUCCESS_WAITING_IME_BINDING,
            ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
            ResultCode.ERROR_NULL,
            ResultCode.ERROR_NO_IME,
            ResultCode.ERROR_INVALID_PACKAGE_NAME,
            ResultCode.ERROR_SYSTEM_NOT_READY,
            ResultCode.ERROR_IME_NOT_CONNECTED,
            ResultCode.ERROR_INVALID_USER,
            ResultCode.ERROR_NULL_EDITOR_INFO,
            ResultCode.ERROR_NOT_IME_TARGET_WINDOW,
            ResultCode.ERROR_NO_EDITOR,
            ResultCode.ERROR_DISPLAY_ID_MISMATCH,
            ResultCode.ERROR_INVALID_DISPLAY_ID,
            ResultCode.SUCCESS_WITH_ACCESSIBILITY_SESSION
    })
    public @interface ResultCode {
        /**
         * Indicates that everything in this result object including {@link #method} is valid.
         */
        int SUCCESS_WITH_IME_SESSION = 0;
        /**
         * Indicates that this is a temporary binding until the
         * {@link android.inputmethodservice.InputMethodService} (IMS) establishes a valid session
         * to {@link com.android.server.inputmethod.InputMethodManagerService} (IMMS).
         *
         * <p>Note that in this state the IMS is already bound to IMMS but the logical session
         * is not yet established on top of the IPC channel.</p>
         *
         * <p>Some of fields such as {@link #channel} is not yet available.</p>
         *
         * @see android.inputmethodservice.InputMethodService#onCreateInputMethodSessionInterface()
         **/
        int SUCCESS_WAITING_IME_SESSION = 1;
        /**
         * Indicates that this is a temporary binding until the
         * {@link android.inputmethodservice.InputMethodService} (IMS) establishes a valid session
         * to {@link com.android.server.inputmethod.InputMethodManagerService} (IMMS).
         *
         * <p>Note that in this state the IMMS has already initiated a connection to the IMS but
         * the binding process is not completed yet.</p>
         *
         * <p>Some of fields such as {@link #channel} is not yet available.</p>
         * @see android.content.ServiceConnection#onServiceConnected(ComponentName, IBinder)
         */
        int SUCCESS_WAITING_IME_BINDING = 2;
        /**
         * Indicates that {@link com.android.server.inputmethod.InputMethodManagerService} has a
         * pending operation to switch to a different user.
         *
         * <p>Note that in this state even what would be the next current IME is not determined.</p>
         */
        int SUCCESS_WAITING_USER_SWITCHING = 3;
        /**
         * Indicates that this is not intended for starting input but just for reporting window
         * focus change from the application process.
         *
         * <p>All other fields do not have meaningful value.</p>
         */
        int SUCCESS_REPORT_WINDOW_FOCUS_ONLY = 4;
        /**
         * Indicates somehow
         * {@link
         * com.android.server.inputmethod.InputMethodManagerService#startInputOrWindowGainedFocus}
         * is trying to return null {@link InputBindResult}, which must never happen.
         */
        int ERROR_NULL = 5;
        /**
         * Indicates that {@link com.android.server.inputmethod.InputMethodManagerService}
         * recognizes no IME.
         */
        int ERROR_NO_IME = 6;
        /**
         * Indicates that {@link android.view.inputmethod.EditorInfo#packageName} does not match
         * the caller UID.
         *
         * @see android.view.inputmethod.EditorInfo#packageName
         */
        int ERROR_INVALID_PACKAGE_NAME = 7;
        /**
         * Indicates that the system is still in an early stage of the boot process and any 3rd
         * party application is not allowed to run.
         *
         * @see com.android.server.SystemService#PHASE_THIRD_PARTY_APPS_CAN_START
         */
        int ERROR_SYSTEM_NOT_READY = 8;
        /**
         * Indicates that {@link com.android.server.inputmethod.InputMethodManagerService} tried to
         * connect to an {@link android.inputmethodservice.InputMethodService} but failed.
         *
         * @see android.content.Context#bindServiceAsUser(Intent, ServiceConnection, int,
         *      android.os.UserHandle)
         */
        int ERROR_IME_NOT_CONNECTED = 9;
        /**
         * Indicates that the caller is not the foreground user, does not have
         * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission, or the user
         * specified in {@link android.view.inputmethod.EditorInfo#targetInputMethodUser} is not
         * running.
         */
        int ERROR_INVALID_USER = 10;
        /**
         * Indicates that the caller should have specified non-null
         * {@link android.view.inputmethod.EditorInfo}.
         */
        int ERROR_NULL_EDITOR_INFO = 11;
        /**
         * Indicates that the target window the client specified cannot be the IME target right now.
         *
         * <p>Due to the asynchronous nature of Android OS, we cannot completely avoid this error.
         * The client should try to restart input when its {@link android.view.Window} is focused
         * again.</p>
         *
         * @see com.android.server.wm.WindowManagerInternal#isInputMethodClientFocus(int, int, int)
         */
        int ERROR_NOT_IME_TARGET_WINDOW = 12;
        /**
         * Indicates that focused view in the current window is not an editor.
         */
        int ERROR_NO_EDITOR = 13;
        /**
         * Indicates that there is a mismatch in display ID between IME client and focused Window.
         */
        int ERROR_DISPLAY_ID_MISMATCH = 14;
        /**
         * Indicates that current IME client is no longer allowed to access to the associated
         * display.
         */
        int ERROR_INVALID_DISPLAY_ID = 15;
        int SUCCESS_WITH_ACCESSIBILITY_SESSION = 16;
    }

    @ResultCode
    public final int result;

    /**
     * The input method service.
     */
    public final IInputMethodSession method;

    /**
     * The accessibility services.
     */
    public SparseArray<IAccessibilityInputMethodSession> accessibilitySessions;

    /**
     * The input channel used to send input events to this IME.
     */
    public final InputChannel channel;

    /**
     * The ID for this input method, as found in InputMethodInfo; null if
     * no input method will be bound.
     */
    public final String id;

    /**
     * Sequence number of this binding.
     */
    public final int sequence;

    @Nullable
    private final float[] mVirtualDisplayToScreenMatrixValues;

    /**
     * {@code true} if the IME explicitly specifies {@code suppressesSpellChecker="true"}.
     */
    public final boolean isInputMethodSuppressingSpellChecker;

    /**
     * @return {@link Matrix} that corresponds to {@link #mVirtualDisplayToScreenMatrixValues}.
     *         {@code null} if {@link #mVirtualDisplayToScreenMatrixValues} is {@code null}.
     */
    @Nullable
    public Matrix getVirtualDisplayToScreenMatrix() {
        if (mVirtualDisplayToScreenMatrixValues == null) {
            return null;
        }
        final Matrix matrix = new Matrix();
        matrix.setValues(mVirtualDisplayToScreenMatrixValues);
        return matrix;
    }

    /**
     * Creates a new instance of {@link InputBindResult}.
     *
     * @param result A result code defined in {@link ResultCode}.
     * @param method {@link IInputMethodSession} to interact with the IME.
     * @param accessibilitySessions {@link IAccessibilityInputMethodSession} to interact with
     *                              accessibility services.
     * @param channel {@link InputChannel} to forward input events to the IME.
     * @param id The {@link String} representations of the IME, which is the same as
     *           {@link android.view.inputmethod.InputMethodInfo#getId()} and
     *           {@link android.content.ComponentName#flattenToShortString()}.
     * @param sequence A sequence number of this binding.
     * @param isInputMethodSuppressingSpellChecker {@code true} if the IME explicitly specifies
     *                                             {@code suppressesSpellChecker="true"}.
     */
    public InputBindResult(@ResultCode int result,
            IInputMethodSession method,
            SparseArray<IAccessibilityInputMethodSession> accessibilitySessions,
            InputChannel channel, String id, int sequence,
            @Nullable Matrix virtualDisplayToScreenMatrix,
            boolean isInputMethodSuppressingSpellChecker) {
        this.result = result;
        this.method = method;
        this.accessibilitySessions = accessibilitySessions;
        this.channel = channel;
        this.id = id;
        this.sequence = sequence;
        if (virtualDisplayToScreenMatrix == null) {
            mVirtualDisplayToScreenMatrixValues = null;
        } else {
            mVirtualDisplayToScreenMatrixValues = new float[9];
            virtualDisplayToScreenMatrix.getValues(mVirtualDisplayToScreenMatrixValues);
        }
        this.isInputMethodSuppressingSpellChecker = isInputMethodSuppressingSpellChecker;
    }

    private InputBindResult(Parcel source) {
        result = source.readInt();
        method = IInputMethodSession.Stub.asInterface(source.readStrongBinder());
        int n = source.readInt();
        if (n < 0) {
            accessibilitySessions = null;
        } else {
            accessibilitySessions = new SparseArray<>(n);
            while (n > 0) {
                int key = source.readInt();
                IAccessibilityInputMethodSession value =
                        IAccessibilityInputMethodSession.Stub.asInterface(
                                source.readStrongBinder());
                accessibilitySessions.append(key, value);
                n--;
            }
        }
        if (source.readInt() != 0) {
            channel = InputChannel.CREATOR.createFromParcel(source);
        } else {
            channel = null;
        }
        id = source.readString();
        sequence = source.readInt();
        mVirtualDisplayToScreenMatrixValues = source.createFloatArray();
        isInputMethodSuppressingSpellChecker = source.readBoolean();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "InputBindResult{result=" + getResultString() + " method=" + method + " id=" + id
                + " sequence=" + sequence
                + " virtualDisplayToScreenMatrix=" + getVirtualDisplayToScreenMatrix()
                + " isInputMethodSuppressingSpellChecker=" + isInputMethodSuppressingSpellChecker
                + "}";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(result);
        dest.writeStrongInterface(method);
        if (accessibilitySessions == null) {
            dest.writeInt(-1);
        } else {
            int n = accessibilitySessions.size();
            dest.writeInt(n);
            int i = 0;
            while (i < n) {
                dest.writeInt(accessibilitySessions.keyAt(i));
                dest.writeStrongInterface(accessibilitySessions.valueAt(i));
                i++;
            }
        }
        if (channel != null) {
            dest.writeInt(1);
            channel.writeToParcel(dest, flags);
        } else {
            dest.writeInt(0);
        }
        dest.writeString(id);
        dest.writeInt(sequence);
        dest.writeFloatArray(mVirtualDisplayToScreenMatrixValues);
        dest.writeBoolean(isInputMethodSuppressingSpellChecker);
    }

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

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

    /**
     * {@inheritDoc}
     */
    @Override
    public int describeContents() {
        return channel != null ? channel.describeContents() : 0;
    }

    private String getResultString() {
        switch (result) {
            case ResultCode.SUCCESS_WITH_IME_SESSION:
                return "SUCCESS_WITH_IME_SESSION";
            case ResultCode.SUCCESS_WAITING_IME_SESSION:
                return "SUCCESS_WAITING_IME_SESSION";
            case ResultCode.SUCCESS_WAITING_IME_BINDING:
                return "SUCCESS_WAITING_IME_BINDING";
            case ResultCode.SUCCESS_WAITING_USER_SWITCHING:
                return "SUCCESS_WAITING_USER_SWITCHING";
            case ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY:
                return "SUCCESS_REPORT_WINDOW_FOCUS_ONLY";
            case ResultCode.ERROR_NULL:
                return "ERROR_NULL";
            case ResultCode.ERROR_NO_IME:
                return "ERROR_NO_IME";
            case ResultCode.ERROR_NO_EDITOR:
                return "ERROR_NO_EDITOR";
            case ResultCode.ERROR_INVALID_PACKAGE_NAME:
                return "ERROR_INVALID_PACKAGE_NAME";
            case ResultCode.ERROR_SYSTEM_NOT_READY:
                return "ERROR_SYSTEM_NOT_READY";
            case ResultCode.ERROR_IME_NOT_CONNECTED:
                return "ERROR_IME_NOT_CONNECTED";
            case ResultCode.ERROR_INVALID_USER:
                return "ERROR_INVALID_USER";
            case ResultCode.ERROR_NULL_EDITOR_INFO:
                return "ERROR_NULL_EDITOR_INFO";
            case ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
                return "ERROR_NOT_IME_TARGET_WINDOW";
            case ResultCode.ERROR_DISPLAY_ID_MISMATCH:
                return "ERROR_DISPLAY_ID_MISMATCH";
            case ResultCode.ERROR_INVALID_DISPLAY_ID:
                return "ERROR_INVALID_DISPLAY_ID";
            default:
                return "Unknown(" + result + ")";
        }
    }

    private static InputBindResult error(@ResultCode int result) {
        return new InputBindResult(result, null, null, null, null, -1, null, false);
    }

    /**
     * Predefined error object for {@link ResultCode#ERROR_NULL}.
     */
    public static final InputBindResult NULL = error(ResultCode.ERROR_NULL);
    /**
     * Predefined error object for {@link ResultCode#NO_IME}.
     */
    public static final InputBindResult NO_IME = error(ResultCode.ERROR_NO_IME);
    /**
     * Predefined error object for {@link ResultCode#NO_EDITOR}.
     */
    public static final InputBindResult NO_EDITOR = error(ResultCode.ERROR_NO_EDITOR);
    /**
     * Predefined error object for {@link ResultCode#ERROR_INVALID_PACKAGE_NAME}.
     */
    public static final InputBindResult INVALID_PACKAGE_NAME =
            error(ResultCode.ERROR_INVALID_PACKAGE_NAME);
    /**
     * Predefined error object for {@link ResultCode#ERROR_NULL_EDITOR_INFO}.
     */
    public static final InputBindResult NULL_EDITOR_INFO = error(ResultCode.ERROR_NULL_EDITOR_INFO);
    /**
     * Predefined error object for {@link ResultCode#ERROR_NOT_IME_TARGET_WINDOW}.
     */
    public static final InputBindResult NOT_IME_TARGET_WINDOW =
            error(ResultCode.ERROR_NOT_IME_TARGET_WINDOW);
    /**
     * Predefined error object for {@link ResultCode#ERROR_IME_NOT_CONNECTED}.
     */
    public static final InputBindResult IME_NOT_CONNECTED =
            error(ResultCode.ERROR_IME_NOT_CONNECTED);
    /**
     * Predefined error object for {@link ResultCode#ERROR_INVALID_USER}.
     */
    public static final InputBindResult INVALID_USER = error(ResultCode.ERROR_INVALID_USER);

    /**
     * Predefined error object for {@link ResultCode#ERROR_DISPLAY_ID_MISMATCH}.
     */
    public static final InputBindResult DISPLAY_ID_MISMATCH =
            error(ResultCode.ERROR_DISPLAY_ID_MISMATCH);

    /**
     * Predefined error object for {@link ResultCode#ERROR_INVALID_DISPLAY_ID}.
     */
    public static final InputBindResult INVALID_DISPLAY_ID =
            error(ResultCode.ERROR_INVALID_DISPLAY_ID);

    /**
     * Predefined <strong>success</strong> object for
     * {@link ResultCode#SUCCESS_WAITING_USER_SWITCHING}.
     */
    public static final InputBindResult USER_SWITCHING =
            error(ResultCode.SUCCESS_WAITING_USER_SWITCHING);
}
