/*
 * Copyright (C) 2018 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.content.pm;

import static android.content.res.ResourceId.ID_NULL;

import android.annotation.DrawableRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.annotation.SystemApi;
import android.content.res.ResourceId;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.util.Slog;

import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.util.Locale;
import java.util.Objects;

/**
 * A container to describe the dialog to be shown when the user tries to launch a suspended
 * application.
 * The suspending app can customize the dialog's following attributes:
 * <ul>
 * <li>The dialog icon, by providing a resource id.
 * <li>The title text, by providing a resource id.
 * <li>The text of the dialog's body, by providing a resource id or a string.
 * <li>The text on the neutral button which starts the
 * {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS SHOW_SUSPENDED_APP_DETAILS}
 * activity, by providing a resource id.
 * </ul>
 * System defaults are used whenever any of these are not provided, or any of the provided resource
 * ids cannot be resolved at the time of displaying the dialog.
 *
 * @hide
 * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle,
 * SuspendDialogInfo)
 * @see Builder
 */
@SystemApi
public final class SuspendDialogInfo implements Parcelable {
    private static final String TAG = SuspendDialogInfo.class.getSimpleName();
    private static final String XML_ATTR_ICON_RES_ID = "iconResId";
    private static final String XML_ATTR_TITLE_RES_ID = "titleResId";
    private static final String XML_ATTR_DIALOG_MESSAGE_RES_ID = "dialogMessageResId";
    private static final String XML_ATTR_DIALOG_MESSAGE = "dialogMessage";
    private static final String XML_ATTR_BUTTON_TEXT_RES_ID = "buttonTextResId";

    private final int mIconResId;
    private final int mTitleResId;
    private final int mDialogMessageResId;
    private final String mDialogMessage;
    private final int mNeutralButtonTextResId;

    /**
     * @return the resource id of the icon to be used with the dialog
     * @hide
     */
    @DrawableRes
    public int getIconResId() {
        return mIconResId;
    }

    /**
     * @return the resource id of the title to be used with the dialog
     * @hide
     */
    @StringRes
    public int getTitleResId() {
        return mTitleResId;
    }

    /**
     * @return the resource id of the text to be shown in the dialog's body
     * @hide
     */
    @StringRes
    public int getDialogMessageResId() {
        return mDialogMessageResId;
    }

    /**
     * @return the text to be shown in the dialog's body. Returns {@code null} if
     * {@link #getDialogMessageResId()} returns a valid resource id.
     * @hide
     */
    @Nullable
    public String getDialogMessage() {
        return mDialogMessage;
    }

    /**
     * @return the text to be shown
     * @hide
     */
    @StringRes
    public int getNeutralButtonTextResId() {
        return mNeutralButtonTextResId;
    }

    /**
     * @hide
     */
    public void saveToXml(XmlSerializer out) throws IOException {
        if (mIconResId != ID_NULL) {
            XmlUtils.writeIntAttribute(out, XML_ATTR_ICON_RES_ID, mIconResId);
        }
        if (mTitleResId != ID_NULL) {
            XmlUtils.writeIntAttribute(out, XML_ATTR_TITLE_RES_ID, mTitleResId);
        }
        if (mDialogMessageResId != ID_NULL) {
            XmlUtils.writeIntAttribute(out, XML_ATTR_DIALOG_MESSAGE_RES_ID, mDialogMessageResId);
        } else {
            XmlUtils.writeStringAttribute(out, XML_ATTR_DIALOG_MESSAGE, mDialogMessage);
        }
        if (mNeutralButtonTextResId != ID_NULL) {
            XmlUtils.writeIntAttribute(out, XML_ATTR_BUTTON_TEXT_RES_ID, mNeutralButtonTextResId);
        }
    }

    /**
     * @hide
     */
    public static SuspendDialogInfo restoreFromXml(XmlPullParser in) {
        final SuspendDialogInfo.Builder dialogInfoBuilder = new SuspendDialogInfo.Builder();
        try {
            final int iconId = XmlUtils.readIntAttribute(in, XML_ATTR_ICON_RES_ID, ID_NULL);
            final int titleId = XmlUtils.readIntAttribute(in, XML_ATTR_TITLE_RES_ID, ID_NULL);
            final int buttonTextId = XmlUtils.readIntAttribute(in, XML_ATTR_BUTTON_TEXT_RES_ID,
                    ID_NULL);
            final int dialogMessageResId = XmlUtils.readIntAttribute(
                    in, XML_ATTR_DIALOG_MESSAGE_RES_ID, ID_NULL);
            final String dialogMessage = XmlUtils.readStringAttribute(in, XML_ATTR_DIALOG_MESSAGE);

            if (iconId != ID_NULL) {
                dialogInfoBuilder.setIcon(iconId);
            }
            if (titleId != ID_NULL) {
                dialogInfoBuilder.setTitle(titleId);
            }
            if (buttonTextId != ID_NULL) {
                dialogInfoBuilder.setNeutralButtonText(buttonTextId);
            }
            if (dialogMessageResId != ID_NULL) {
                dialogInfoBuilder.setMessage(dialogMessageResId);
            } else if (dialogMessage != null) {
                dialogInfoBuilder.setMessage(dialogMessage);
            }
        } catch (Exception e) {
            Slog.e(TAG, "Exception while parsing from xml. Some fields may default", e);
        }
        return dialogInfoBuilder.build();
    }

    @Override
    public int hashCode() {
        int hashCode = mIconResId;
        hashCode = 31 * hashCode + mTitleResId;
        hashCode = 31 * hashCode + mNeutralButtonTextResId;
        hashCode = 31 * hashCode + mDialogMessageResId;
        hashCode = 31 * hashCode + Objects.hashCode(mDialogMessage);
        return hashCode;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof SuspendDialogInfo)) {
            return false;
        }
        final SuspendDialogInfo otherDialogInfo = (SuspendDialogInfo) obj;
        return mIconResId == otherDialogInfo.mIconResId
                && mTitleResId == otherDialogInfo.mTitleResId
                && mDialogMessageResId == otherDialogInfo.mDialogMessageResId
                && mNeutralButtonTextResId == otherDialogInfo.mNeutralButtonTextResId
                && Objects.equals(mDialogMessage, otherDialogInfo.mDialogMessage);
    }

    @Override
    public String toString() {
        final StringBuilder builder = new StringBuilder("SuspendDialogInfo: {");
        if (mIconResId != ID_NULL) {
            builder.append("mIconId = 0x");
            builder.append(Integer.toHexString(mIconResId));
            builder.append(" ");
        }
        if (mTitleResId != ID_NULL) {
            builder.append("mTitleResId = 0x");
            builder.append(Integer.toHexString(mTitleResId));
            builder.append(" ");
        }
        if (mNeutralButtonTextResId != ID_NULL) {
            builder.append("mNeutralButtonTextResId = 0x");
            builder.append(Integer.toHexString(mNeutralButtonTextResId));
            builder.append(" ");
        }
        if (mDialogMessageResId != ID_NULL) {
            builder.append("mDialogMessageResId = 0x");
            builder.append(Integer.toHexString(mDialogMessageResId));
            builder.append(" ");
        } else if (mDialogMessage != null) {
            builder.append("mDialogMessage = \"");
            builder.append(mDialogMessage);
            builder.append("\" ");
        }
        builder.append("}");
        return builder.toString();
    }

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

    @Override
    public void writeToParcel(Parcel dest, int parcelableFlags) {
        dest.writeInt(mIconResId);
        dest.writeInt(mTitleResId);
        dest.writeInt(mDialogMessageResId);
        dest.writeString(mDialogMessage);
        dest.writeInt(mNeutralButtonTextResId);
    }

    private SuspendDialogInfo(Parcel source) {
        mIconResId = source.readInt();
        mTitleResId = source.readInt();
        mDialogMessageResId = source.readInt();
        mDialogMessage = source.readString();
        mNeutralButtonTextResId = source.readInt();
    }

    SuspendDialogInfo(Builder b) {
        mIconResId = b.mIconResId;
        mTitleResId = b.mTitleResId;
        mDialogMessageResId = b.mDialogMessageResId;
        mDialogMessage = (mDialogMessageResId == ID_NULL) ? b.mDialogMessage : null;
        mNeutralButtonTextResId = b.mNeutralButtonTextResId;
    }

    public static final Creator<SuspendDialogInfo> CREATOR = new Creator<SuspendDialogInfo>() {
        @Override
        public SuspendDialogInfo createFromParcel(Parcel source) {
            return new SuspendDialogInfo(source);
        }

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

    /**
     * Builder to build a {@link SuspendDialogInfo} object.
     */
    public static final class Builder {
        private int mDialogMessageResId = ID_NULL;
        private String mDialogMessage;
        private int mTitleResId = ID_NULL;
        private int mIconResId = ID_NULL;
        private int mNeutralButtonTextResId = ID_NULL;

        /**
         * Set the resource id of the icon to be used. If not provided, no icon will be shown.
         *
         * @param resId The resource id of the icon.
         * @return this builder object.
         */
        @NonNull
        public Builder setIcon(@DrawableRes int resId) {
            Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
            mIconResId = resId;
            return this;
        }

        /**
         * Set the resource id of the title text to be displayed. If this is not provided, the
         * system will use a default title.
         *
         * @param resId The resource id of the title.
         * @return this builder object.
         */
        @NonNull
        public Builder setTitle(@StringRes int resId) {
            Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
            mTitleResId = resId;
            return this;
        }

        /**
         * Set the text to show in the body of the dialog. Ignored if a resource id is set via
         * {@link #setMessage(int)}.
         * <p>
         * The system will use {@link String#format(Locale, String, Object...) String.format} to
         * insert the suspended app name into the message, so an example format string could be
         * {@code "The app %1$s is currently suspended"}. This is optional - if the string passed in
         * {@code message} does not accept an argument, it will be used as is.
         *
         * @param message The dialog message.
         * @return this builder object.
         * @see #setMessage(int)
         */
        @NonNull
        public Builder setMessage(@NonNull String message) {
            Preconditions.checkStringNotEmpty(message, "Message cannot be null or empty");
            mDialogMessage = message;
            return this;
        }

        /**
         * Set the resource id of the dialog message to be shown. If no dialog message is provided
         * via either this method or {@link #setMessage(String)}, the system will use a
         * default message.
         * <p>
         * The system will use {@link android.content.res.Resources#getString(int, Object...)
         * getString} to insert the suspended app name into the message, so an example format string
         * could be {@code "The app %1$s is currently suspended"}. This is optional - if the string
         * referred to by {@code resId} does not accept an argument, it will be used as is.
         *
         * @param resId The resource id of the dialog message.
         * @return this builder object.
         * @see #setMessage(String)
         */
        @NonNull
        public Builder setMessage(@StringRes int resId) {
            Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
            mDialogMessageResId = resId;
            return this;
        }

        /**
         * Set the resource id of text to be shown on the neutral button. Tapping this button starts
         * the {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS} activity. If this is
         * not provided, the system will use a default text.
         *
         * @param resId The resource id of the button text
         * @return this builder object.
         */
        @NonNull
        public Builder setNeutralButtonText(@StringRes int resId) {
            Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
            mNeutralButtonTextResId = resId;
            return this;
        }

        /**
         * Build the final object based on given inputs.
         *
         * @return The {@link SuspendDialogInfo} object built using this builder.
         */
        @NonNull
        public SuspendDialogInfo build() {
            return new SuspendDialogInfo(this);
        }
    }
}
