/*
 * Copyright (C) 2021 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.server.servicewatcher;

import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ResolveInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;

import com.android.server.FgThread;

import java.io.PrintWriter;
import java.util.Objects;

/**
 * A ServiceWatcher is responsible for continuously maintaining an active binding to a service
 * selected by it's {@link ServiceSupplier}. The {@link ServiceSupplier} may change the service it
 * selects over time, and the currently bound service may crash, restart, have a user change, have
 * changes made to its package, and so on and so forth. The ServiceWatcher is responsible for
 * maintaining the binding across all these changes.
 *
 * <p>Clients may invoke {@link BinderOperation}s on the ServiceWatcher, and it will make a best
 * effort to run these on the currently bound service, but individual operations may fail (if there
 * is no service currently bound for instance). In order to help clients maintain the correct state,
 * clients may supply a {@link ServiceListener}, which is informed when the ServiceWatcher connects
 * and disconnects from a service. This allows clients to bring a bound service back into a known
 * state on connection, and then run binder operations from there. In order to help clients
 * accomplish this, ServiceWatcher guarantees that {@link BinderOperation}s and the
 * {@link ServiceListener} will always be run on the same thread, so that strong ordering guarantees
 * can be established between them.
 *
 * There is never any guarantee of whether a ServiceWatcher is currently connected to a service, and
 * whether any particular {@link BinderOperation} will succeed. Clients must ensure they do not rely
 * on this, and instead use {@link ServiceListener} notifications as necessary to recover from
 * failures.
 */
public interface ServiceWatcher {

    /**
     * Operation to run on a binder interface. All operations will be run on the thread used by the
     * ServiceWatcher this is run with.
     */
    interface BinderOperation {
        /** Invoked to run the operation. Run on the ServiceWatcher thread. */
        void run(IBinder binder) throws RemoteException;

        /**
         * Invoked if {@link #run(IBinder)} could not be invoked because there was no current
         * binding, or if {@link #run(IBinder)} threw an exception ({@link RemoteException} or
         * {@link RuntimeException}). This callback is only intended for resource deallocation and
         * cleanup in response to a single binder operation, it should not be used to propagate
         * errors further. Run on the ServiceWatcher thread.
         */
        default void onError() {}
    }

    /**
     * Listener for bind and unbind events. All operations will be run on the thread used by the
     * ServiceWatcher this is run with.
     *
     * @param <TBoundServiceInfo> type of bound service
     */
    interface ServiceListener<TBoundServiceInfo extends BoundServiceInfo> {
        /** Invoked when a service is bound. Run on the ServiceWatcher thread. */
        void onBind(IBinder binder, TBoundServiceInfo service) throws RemoteException;

        /** Invoked when a service is unbound. Run on the ServiceWatcher thread. */
        void onUnbind();
    }

    /**
     * A listener for when a {@link ServiceSupplier} decides that the current service has changed.
     */
    interface ServiceChangedListener {
        /**
         * Should be invoked when the current service may have changed.
         */
        void onServiceChanged();
    }

    /**
     * This supplier encapsulates the logic of deciding what service a {@link ServiceWatcher} should
     * be bound to at any given moment.
     *
     * @param <TBoundServiceInfo> type of bound service
     */
    interface ServiceSupplier<TBoundServiceInfo extends BoundServiceInfo> {
        /**
         * Should return true if there exists at least one service capable of meeting the criteria
         * of this supplier. This does not imply that {@link #getServiceInfo()} will always return a
         * non-null result, as any service may be disqualified for various reasons at any point in
         * time. May be invoked at any time from any thread and thus should generally not have any
         * dependency on the other methods in this interface.
         */
        boolean hasMatchingService();

        /**
         * Invoked when the supplier should start monitoring for any changes that could result in a
         * different service selection, and should invoke
         * {@link ServiceChangedListener#onServiceChanged()} in that case. {@link #getServiceInfo()}
         * may be invoked after this method is called.
         */
        void register(ServiceChangedListener listener);

        /**
         * Invoked when the supplier should stop monitoring for any changes that could result in a
         * different service selection, should no longer invoke
         * {@link ServiceChangedListener#onServiceChanged()}. {@link #getServiceInfo()} will not be
         * invoked after this method is called.
         */
        void unregister();

        /**
         * Must be implemented to return the current service selected by this supplier. May return
         * null if no service currently meets the criteria. Only invoked while registered.
         */
        @Nullable TBoundServiceInfo getServiceInfo();
    }

    /**
     * Information on the service selected as the best option for binding.
     */
    class BoundServiceInfo {

        protected final @Nullable String mAction;
        protected final int mUid;
        protected final ComponentName mComponentName;

        protected BoundServiceInfo(String action, ResolveInfo resolveInfo) {
            this(action, resolveInfo.serviceInfo.applicationInfo.uid,
                    resolveInfo.serviceInfo.getComponentName());
        }

        protected BoundServiceInfo(String action, int uid, ComponentName componentName) {
            mAction = action;
            mUid = uid;
            mComponentName = Objects.requireNonNull(componentName);
        }

        /** Returns the action associated with this bound service. */
        public @Nullable String getAction() {
            return mAction;
        }

        /** Returns the component of this bound service. */
        public ComponentName getComponentName() {
            return mComponentName;
        }

        /** Returns the user id for this bound service. */
        public @UserIdInt int getUserId() {
            return UserHandle.getUserId(mUid);
        }

        @Override
        public final boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof BoundServiceInfo)) {
                return false;
            }

            BoundServiceInfo that = (BoundServiceInfo) o;
            return mUid == that.mUid
                    && Objects.equals(mAction, that.mAction)
                    && mComponentName.equals(that.mComponentName);
        }

        @Override
        public final int hashCode() {
            return Objects.hash(mAction, mUid, mComponentName);
        }

        @Override
        public String toString() {
            if (mComponentName == null) {
                return "none";
            } else {
                return mUid + "/" + mComponentName.flattenToShortString();
            }
        }
    }

    /**
     * Creates a new ServiceWatcher instance.
     */
    static <TBoundServiceInfo extends BoundServiceInfo> ServiceWatcher create(
            Context context,
            String tag,
            ServiceSupplier<TBoundServiceInfo> serviceSupplier,
            @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) {
        return create(context, FgThread.getHandler(), tag, serviceSupplier, serviceListener);
    }

    /**
     * Creates a new ServiceWatcher instance that runs on the given handler.
     */
    static <TBoundServiceInfo extends BoundServiceInfo> ServiceWatcher create(
            Context context,
            Handler handler,
            String tag,
            ServiceSupplier<TBoundServiceInfo> serviceSupplier,
            @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) {
        return new ServiceWatcherImpl<>(context, handler, tag, serviceSupplier, serviceListener);
    }

    /**
     * Returns true if there is at least one service that the ServiceWatcher could hypothetically
     * bind to, as selected by the {@link ServiceSupplier}.
     */
    boolean checkServiceResolves();

    /**
     * Registers the ServiceWatcher, so that it will begin maintaining an active binding to the
     * service selected by {@link ServiceSupplier}, until {@link #unregister()} is called.
     */
    void register();

    /**
     * Unregisters the ServiceWatcher, so that it will release any active bindings. If the
     * ServiceWatcher is currently bound, this will result in one final
     * {@link ServiceListener#onUnbind()} invocation, which may happen after this method completes
     * (but which is guaranteed to occur before any further
     * {@link ServiceListener#onBind(IBinder, BoundServiceInfo)} invocation in response to a later
     * call to {@link #register()}).
     */
    void unregister();

    /**
     * Runs the given binder operation on the currently bound service (if available). The operation
     * will always fail if the ServiceWatcher is not currently registered.
     */
    void runOnBinder(BinderOperation operation);

    /**
     * Dumps ServiceWatcher information.
     */
    void dump(PrintWriter pw);
}