/*
 * Copyright (C) 2020 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.parsing.result;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.pm.PackageManager;
import android.os.Build;

/**
 * Used as a method parameter which is then transformed into a {@link ParseResult}. This is
 * generalized as it doesn't matter what type this input is for. It's simply to hide the
 * methods of {@link ParseResult}.
 *
 * @hide
 */
public interface ParseInput {

    /**
     * Errors encountered during parsing may rely on the targetSDK version of the application to
     * determine whether or not to fail. These are passed into {@link #deferError(String, long)}
     * when encountered, and the implementation will handle how to defer the errors until the
     * targetSdkVersion is known and sent to {@link #enableDeferredError(String, int)}.
     *
     * All of these must be marked {@link ChangeId}, as that is the mechanism used to check if the
     * error must be propagated. This framework also allows developers to pre-disable specific
     * checks if they wish to target a newer SDK version in a development environment without
     * having to migrate their entire app to validate on a newer platform.
     */
    final class DeferredError {
        /**
         * Missing an "application" or "instrumentation" tag.
         */
        @ChangeId
        @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
        public static final long MISSING_APP_TAG = 150776642;

        /**
         * An intent filter's actor or category is an empty string. A bug in the platform before R
         * allowed this to pass through without an error. This does not include cases when the
         * attribute is null/missing, as that has always been a failure.
         */
        @ChangeId
        @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
        public static final long EMPTY_INTENT_ACTION_CATEGORY = 151163173;

        /**
         * The {@code resources.arsc} of one of the APKs being installed is compressed or not
         * aligned on a 4-byte boundary. Resource tables that cannot be memory mapped exert excess
         * memory pressure on the system and drastically slow down construction of
         * {@link android.content.res.Resources} objects.
         */
        @ChangeId
        @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
        public static final long RESOURCES_ARSC_COMPRESSED = 132742131;

        /**
         * Missing `android:exported` flag. When an intent filter is defined, an explicit value
         * for the android:exported flag is required.
         */
        @ChangeId
        @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
        public static final long MISSING_EXPORTED_FLAG = 150232615;

        /**
         * TODO(chiuwinson): This is required because PackageManager#getPackageArchiveInfo
         *   cannot read the targetSdk info from the changeId because it requires the
         *   READ_COMPAT_CHANGE_CONFIG which cannot be obtained automatically without entering the
         *   server process. This should be removed once an alternative is found, or if the API
         *   is removed.
         * @return the targetSdk that this change is gated on (> check), or -1 if disabled
         */
        @IntRange(from = -1, to = Integer.MAX_VALUE)
        public static int getTargetSdkForChange(long changeId) {
            if (changeId == MISSING_APP_TAG
                    || changeId == EMPTY_INTENT_ACTION_CATEGORY
                    || changeId == RESOURCES_ARSC_COMPRESSED) {
                return Build.VERSION_CODES.Q;
            }

            if (changeId == MISSING_EXPORTED_FLAG) {
                return Build.VERSION_CODES.R;
            }

            return -1;
        }
    }

    <ResultType> ParseResult<ResultType> success(ResultType result);

    /**
     * Used for errors gated by {@link DeferredError}. Will return an error result if the
     * targetSdkVersion is already known and this must be returned as a real error. The result
     * contains null and should not be unwrapped.
     *
     * @see #error(String)
     */
    ParseResult<?> deferError(@NonNull String parseError, long deferredError);

    /**
     * Called after targetSdkVersion is known. Returns an error result if a previously deferred
     * error was registered. The result contains null and should not be unwrapped.
     */
    ParseResult<?> enableDeferredError(String packageName, int targetSdkVersion);

    /**
     * This will assign errorCode to {@link PackageManager#INSTALL_PARSE_FAILED_SKIPPED, used for
     * packages which should be ignored by the caller.
     *
     * @see #error(int, String, Exception)
     */
    <ResultType> ParseResult<ResultType> skip(@NonNull String parseError);

    /** @see #error(int, String, Exception) */
    <ResultType> ParseResult<ResultType> error(int parseError);

    /**
     * This will assign errorCode to {@link PackageManager#INSTALL_PARSE_FAILED_MANIFEST_MALFORMED}.
     * @see #error(int, String, Exception)
     */
    <ResultType> ParseResult<ResultType> error(@NonNull String parseError);

    /** @see #error(int, String, Exception) */
    <ResultType> ParseResult<ResultType> error(int parseError, @Nullable String errorMessage);

    /**
     * Marks this as an error result. When this method is called, the return value <b>must</b>
     * be returned to the exit of the parent method that took in this {@link ParseInput} as a
     * parameter.
     *
     * The calling site of that method is then expected to check the result for error, and
     * continue to bubble up if it is an error.
     *
     * If the result {@link ParseResult#isSuccess()}, then it can be used as-is, as
     * overlapping/consecutive successes are allowed.
     */
    <ResultType> ParseResult<ResultType> error(int parseError, @Nullable String errorMessage,
            @Nullable Exception exception);

    /**
     * Moves the error in {@code result} to this input's type. In practice this does nothing
     * but cast the type of the {@link ParseResult} for type safety, since the parameter
     * and the receiver should be the same object.
     */
    <ResultType> ParseResult<ResultType> error(ParseResult<?> result);

    /**
     * Implemented instead of a direct reference to
     * {@link com.android.internal.compat.IPlatformCompat}, allowing caching and testing logic to
     * be separated out.
     */
    interface Callback {
        /**
         * @return true if the changeId should be enabled
         */
        boolean isChangeEnabled(long changeId, @NonNull String packageName, int targetSdkVersion);
    }
}
