/**
 * Copyright 2017 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.dex;

import static android.Manifest.permission.PACKAGE_USAGE_STATS;
import static android.Manifest.permission.READ_RUNTIME_PROFILES;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.content.Context;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Slog;

import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;

/**
 * Class for retrieving various kinds of information related to the runtime artifacts of
 * packages that are currently installed on the device.
 *
 * @hide
 */
@SystemApi
public class ArtManager {
    private static final String TAG = "ArtManager";

    /** The snapshot failed because the package was not found. */
    public static final int SNAPSHOT_FAILED_PACKAGE_NOT_FOUND = 0;
    /** The snapshot failed because the package code path does not exist. */
    public static final int SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND = 1;
    /** The snapshot failed because of an internal error (e.g. error during opening profiles). */
    public static final int SNAPSHOT_FAILED_INTERNAL_ERROR = 2;

    /** Constant used for applications profiles. */
    public static final int PROFILE_APPS = 0;
    /** Constant used for the boot image profile. */
    public static final int PROFILE_BOOT_IMAGE = 1;

    /** @hide */
    @IntDef(flag = true, prefix = { "PROFILE_" }, value = {
            PROFILE_APPS,
            PROFILE_BOOT_IMAGE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ProfileType {}

    private final Context mContext;
    private final IArtManager mArtManager;

    /**
     * @hide
     */
    public ArtManager(@NonNull Context context, @NonNull IArtManager manager) {
        mContext = context;
        mArtManager = manager;
    }

    /**
     * Snapshots a runtime profile according to the {@code profileType} parameter.
     *
     * If {@code profileType} is {@link ArtManager#PROFILE_APPS} the method will snapshot
     * the profile for for an apk belonging to the package {@code packageName}.
     * The apk is identified by {@code codePath}.
     *
     * If {@code profileType} is {@code ArtManager.PROFILE_BOOT_IMAGE} the method will snapshot
     * the profile for the boot image. In this case {@code codePath can be null}. The parameters
     * {@code packageName} and {@code codePath} are ignored.
     *u
     * The calling process must have {@code android.permission.READ_RUNTIME_PROFILE} permission.
     *
     * The result will be posted on the {@code executor} using the given {@code callback}.
     * The profile will be available as a read-only {@link android.os.ParcelFileDescriptor}.
     *
     * This method will throw {@link IllegalStateException} if
     * {@link ArtManager#isRuntimeProfilingEnabled(int)} does not return true for the given
     * {@code profileType}.
     *
     * @param profileType the type of profile that should be snapshot (boot image or app)
     * @param packageName the target package name or null if the target is the boot image
     * @param codePath the code path for which the profile should be retrieved or null if
     *                 the target is the boot image
     * @param callback the callback which should be used for the result
     * @param executor the executor which should be used to post the result
     */
    @RequiresPermission(allOf = { READ_RUNTIME_PROFILES, PACKAGE_USAGE_STATS })
    public void snapshotRuntimeProfile(@ProfileType int profileType, @Nullable String packageName,
            @Nullable String codePath, @NonNull @CallbackExecutor Executor executor,
            @NonNull SnapshotRuntimeProfileCallback callback) {
        Slog.d(TAG, "Requesting profile snapshot for " + packageName + ":" + codePath);

        SnapshotRuntimeProfileCallbackDelegate delegate =
                new SnapshotRuntimeProfileCallbackDelegate(callback, executor);
        try {
            mArtManager.snapshotRuntimeProfile(profileType, packageName, codePath, delegate,
                    mContext.getOpPackageName());
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    /**
     * Returns true if runtime profiles are enabled for the given type, false otherwise.
     *
     * The calling process must have {@code android.permission.READ_RUNTIME_PROFILE} permission.
     *
     * @param profileType can be either {@link ArtManager#PROFILE_APPS}
     *                    or {@link ArtManager#PROFILE_BOOT_IMAGE}
     */
    @RequiresPermission(allOf = { READ_RUNTIME_PROFILES, PACKAGE_USAGE_STATS })
    public boolean isRuntimeProfilingEnabled(@ProfileType int profileType) {
        try {
            return mArtManager.isRuntimeProfilingEnabled(profileType, mContext.getOpPackageName());
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    /**
     * Callback used for retrieving runtime profiles.
     */
    public abstract static class SnapshotRuntimeProfileCallback {
        /**
         * Called when the profile snapshot finished with success.
         *
         * @param profileReadFd the file descriptor that can be used to read the profile. Note that
         *                      the file might be empty (which is valid profile).
         */
        public abstract void onSuccess(ParcelFileDescriptor profileReadFd);

        /**
         * Called when the profile snapshot finished with an error.
         *
         * @param errCode the error code {@see SNAPSHOT_FAILED_PACKAGE_NOT_FOUND,
         *      SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND, SNAPSHOT_FAILED_INTERNAL_ERROR}.
         */
        public abstract void onError(int errCode);
    }

    private static class SnapshotRuntimeProfileCallbackDelegate
            extends android.content.pm.dex.ISnapshotRuntimeProfileCallback.Stub {
        private final ArtManager.SnapshotRuntimeProfileCallback mCallback;
        private final Executor mExecutor;

        private SnapshotRuntimeProfileCallbackDelegate(
                ArtManager.SnapshotRuntimeProfileCallback callback, Executor executor) {
            mCallback = callback;
            mExecutor = executor;
        }

        @Override
        public void onSuccess(final ParcelFileDescriptor profileReadFd) {
            mExecutor.execute(() -> mCallback.onSuccess(profileReadFd));
        }

        @Override
        public void onError(int errCode) {
            mExecutor.execute(() -> mCallback.onError(errCode));
        }
    }

    /**
     * Return the profile name for the given split. If {@code splitName} is null the
     * method returns the profile name for the base apk.
     *
     * @hide
     */
    public static String getProfileName(String splitName) {
        return splitName == null ? "primary.prof" : splitName + ".split.prof";
    }

    /**
     * Return the path to the current profile corresponding to given package and split.
     *
     * @hide
     */
    public static String getCurrentProfilePath(String packageName, int userId, String splitName) {
        File profileDir = Environment.getDataProfilesDePackageDirectory(userId, packageName);
        return new File(profileDir, getProfileName(splitName)).getAbsolutePath();
    }

    /**
     * Return the snapshot profile file for the given package and profile name.
     *
     * KEEP in sync with installd dexopt.cpp.
     * TODO(calin): inject the snapshot profile name from PM to avoid the dependency.
     *
     * @hide
     */
    public static File getProfileSnapshotFileForName(String packageName, String profileName) {
        File profileDir = Environment.getDataRefProfilesDePackageDirectory(packageName);
        return new File(profileDir, profileName  + ".snapshot");
    }
}
