/*
 * Copyright (C) 2019 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.service.watchdog;

import static android.os.Parcelable.Creator;

import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.crashrecovery.flags.Flags;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.util.Log;

import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
 * A service to provide packages supporting explicit health checks and route checks to these
 * packages on behalf of the package watchdog.
 *
 * <p>To extend this class, you must declare the service in your manifest file with the {@link
 * android.Manifest.permission#BIND_EXPLICIT_HEALTH_CHECK_SERVICE} permission, and include an intent
 * filter with the {@link #SERVICE_INTERFACE} action. In addition, your implementation must live in
 * {@link PackageManager#getServicesSystemSharedLibraryPackageName()}. For example:
 *
 * <pre>
 *     &lt;service android:name=".FooExplicitHealthCheckService"
 *             android:exported="true"
 *             android:priority="100"
 *             android:permission="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"&gt;
 *         &lt;intent-filter&gt;
 *             &lt;action android:name="android.service.watchdog.ExplicitHealthCheckService" /&gt;
 *         &lt;/intent-filter&gt;
 *     &lt;/service&gt;
 * </pre>
 *
 * @hide
 */
@SystemApi
public abstract class ExplicitHealthCheckService extends Service {

    private static final String TAG = "ExplicitHealthCheckService";

    /**
     * {@link Bundle} key for a {@link List} of {@link PackageConfig} value.
     *
     * @hide
     */
    public static final String EXTRA_SUPPORTED_PACKAGES =
            "android.service.watchdog.extra.supported_packages";

    /**
     * {@link Bundle} key for a {@link List} of {@link String} value.
     *
     * @hide
     */
    public static final String EXTRA_REQUESTED_PACKAGES =
            "android.service.watchdog.extra.requested_packages";

    /** {@link Bundle} key for a {@link String} value. */
    @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY)
    public static final String EXTRA_HEALTH_CHECK_PASSED_PACKAGE =
            "android.service.watchdog.extra.HEALTH_CHECK_PASSED_PACKAGE";

    /**
     * The Intent action that a service must respond to. Add it to the intent filter of the service
     * in its manifest.
     */
    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
    public static final String SERVICE_INTERFACE =
            "android.service.watchdog.ExplicitHealthCheckService";

    /**
     * The permission that a service must require to ensure that only Android system can bind to it.
     * If this permission is not enforced in the AndroidManifest of the service, the system will
     * skip that service.
     */
    public static final String BIND_PERMISSION =
            "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE";

    private final ExplicitHealthCheckServiceWrapper mWrapper =
            new ExplicitHealthCheckServiceWrapper();

    /**
     * Called when the system requests an explicit health check for {@code packageName}.
     *
     * <p>When {@code packageName} passes the check, implementors should call {@link
     * #notifyHealthCheckPassed} to inform the system.
     *
     * <p>It could take many hours before a {@code packageName} passes a check and implementors
     * should never drop requests unless {@link onCancel} is called or the service dies.
     *
     * <p>Requests should not be queued and additional calls while expecting a result for {@code
     * packageName} should have no effect.
     */
    public abstract void onRequestHealthCheck(@NonNull String packageName);

    /**
     * Called when the system cancels the explicit health check request for {@code packageName}.
     * Should do nothing if there is no active request for {@code packageName}.
     */
    public abstract void onCancelHealthCheck(@NonNull String packageName);

    /**
     * Called when the system requests for all the packages supporting explicit health checks. The
     * system may request an explicit health check for any of these packages with {@link
     * #onRequestHealthCheck}.
     *
     * @return all packages supporting explicit health checks
     */
    @NonNull
    public abstract List<PackageConfig> onGetSupportedPackages();

    /**
     * Called when the system requests for all the packages that it has currently requested an
     * explicit health check for.
     *
     * @return all packages expecting an explicit health check result
     */
    @NonNull
    public abstract List<String> onGetRequestedPackages();

    private final Handler mHandler = Handler.createAsync(Looper.getMainLooper());
    @Nullable private Consumer<Bundle> mHealthCheckResultCallback;
    @Nullable private Executor mCallbackExecutor;

    @Override
    @NonNull
    public final IBinder onBind(@NonNull Intent intent) {
        return mWrapper;
    }

    /**
     * Sets a callback to be invoked when an explicit health check passes for a package.
     *
     * <p>The callback will receive a {@link Bundle} containing the package name that passed the
     * health check, identified by the key {@link #EXTRA_HEALTH_CHECK_PASSED_PACKAGE}.
     *
     * <p><b>Note:</b> This API is primarily intended for testing purposes. Calling this outside of
     * a test environment will override the default callback mechanism used to notify the system
     * about health check results. Use with caution in production code.
     *
     * @param executor The executor on which the callback should be invoked. If {@code null}, the
     *     callback will be executed on the main thread.
     * @param callback A callback that receives a {@link Bundle} containing the package name that
     *     passed the health check.
     */
    @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY)
    public final void setHealthCheckPassedCallback(
            @CallbackExecutor @Nullable Executor executor, @Nullable Consumer<Bundle> callback) {
        mCallbackExecutor = executor;
        mHealthCheckResultCallback = callback;
    }

    private void executeCallback(@NonNull String packageName) {
        if (mHealthCheckResultCallback != null) {
            Objects.requireNonNull(
                    packageName, "Package passing explicit health check must be non-null");
            Bundle bundle = new Bundle();
            bundle.putString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE, packageName);
            mHealthCheckResultCallback.accept(bundle);
        } else {
            Log.wtf(TAG, "System missed explicit health check result for " + packageName);
        }
    }

    /**
     * Implementors should call this to notify the system when explicit health check passes for
     * {@code packageName};
     */
    public final void notifyHealthCheckPassed(@NonNull String packageName) {
        if (mCallbackExecutor != null) {
            mCallbackExecutor.execute(() -> executeCallback(packageName));
        } else {
            mHandler.post(() -> executeCallback(packageName));
        }
    }

    /**
     * A PackageConfig contains a package supporting explicit health checks and the timeout in
     * {@link System#uptimeMillis} across reboots after which health check requests from clients are
     * failed.
     *
     * @hide
     */
    @SystemApi
    public static final class PackageConfig implements Parcelable {
        private static final long DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(1);

        private final String mPackageName;
        private final long mHealthCheckTimeoutMillis;

        /**
         * Creates a new instance.
         *
         * @param packageName the package name
         * @param healthCheckTimeoutMillis the duration in milliseconds, must be greater than or
         *     equal to 0. If it is 0, it will use a system default value.
         */
        public PackageConfig(@NonNull String packageName, long healthCheckTimeoutMillis) {
            mPackageName = Preconditions.checkNotNull(packageName);
            if (healthCheckTimeoutMillis == 0) {
                mHealthCheckTimeoutMillis = DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS;
            } else {
                mHealthCheckTimeoutMillis =
                        Preconditions.checkArgumentNonnegative(healthCheckTimeoutMillis);
            }
        }

        private PackageConfig(Parcel parcel) {
            mPackageName = parcel.readString();
            mHealthCheckTimeoutMillis = parcel.readLong();
        }

        /**
         * Gets the package name.
         *
         * @return the package name
         */
        public @NonNull String getPackageName() {
            return mPackageName;
        }

        /**
         * Gets the timeout in milliseconds to evaluate an explicit health check result after a
         * request.
         *
         * @return the duration in {@link System#uptimeMillis} across reboots
         */
        public long getHealthCheckTimeoutMillis() {
            return mHealthCheckTimeoutMillis;
        }

        @NonNull
        @Override
        public String toString() {
            return "PackageConfig{" + mPackageName + ", " + mHealthCheckTimeoutMillis + "}";
        }

        @Override
        public boolean equals(@Nullable Object other) {
            if (other == this) {
                return true;
            }
            if (!(other instanceof PackageConfig)) {
                return false;
            }

            PackageConfig otherInfo = (PackageConfig) other;
            return mHealthCheckTimeoutMillis == otherInfo.getHealthCheckTimeoutMillis()
                    && Objects.equals(otherInfo.getPackageName(), mPackageName);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mPackageName, mHealthCheckTimeoutMillis);
        }

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

        @Override
        public void writeToParcel(@SuppressLint({"MissingNullability"}) Parcel parcel, int flags) {
            parcel.writeString(mPackageName);
            parcel.writeLong(mHealthCheckTimeoutMillis);
        }

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

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

    private class ExplicitHealthCheckServiceWrapper extends IExplicitHealthCheckService.Stub {
        @Override
        public void setCallback(RemoteCallback callback) throws RemoteException {
            mHandler.post(() -> mHealthCheckResultCallback = callback::sendResult);
        }

        @Override
        public void request(String packageName) throws RemoteException {
            mHandler.post(() -> ExplicitHealthCheckService.this.onRequestHealthCheck(packageName));
        }

        @Override
        public void cancel(String packageName) throws RemoteException {
            mHandler.post(() -> ExplicitHealthCheckService.this.onCancelHealthCheck(packageName));
        }

        @Override
        public void getSupportedPackages(RemoteCallback callback) throws RemoteException {
            mHandler.post(
                    () -> {
                        List<PackageConfig> packages =
                                ExplicitHealthCheckService.this.onGetSupportedPackages();
                        Objects.requireNonNull(packages, "Supported package list must be non-null");
                        Bundle bundle = new Bundle();
                        bundle.putParcelableArrayList(
                                EXTRA_SUPPORTED_PACKAGES, new ArrayList<>(packages));
                        callback.sendResult(bundle);
                    });
        }

        @Override
        public void getRequestedPackages(RemoteCallback callback) throws RemoteException {
            mHandler.post(
                    () -> {
                        List<String> packages =
                                ExplicitHealthCheckService.this.onGetRequestedPackages();
                        Objects.requireNonNull(
                                packages, "Requested package list must be non-null");
                        Bundle bundle = new Bundle();
                        bundle.putStringArrayList(
                                EXTRA_REQUESTED_PACKAGES, new ArrayList<>(packages));
                        callback.sendResult(bundle);
                    });
        }
    }
}
