/*
 * 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 com.android.internal.infra;

import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ApplicationExitInfo;
import android.app.IActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ParceledListSlice;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.IInterface;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Slog;
import android.util.TimeUtils;

import com.android.internal.annotations.GuardedBy;

import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * Base class representing a remote service.
 *
 * <p>It abstracts away the binding and unbinding from the remote implementation, so clients can
 * call its methods without worrying about when and how to bind/unbind/timeout.
 *
 * <p>All state of this class is modified on a handler thread.
 *
 * <p><b>NOTE: </b>this class should not be extended directly, you should extend either
 * {@link AbstractSinglePendingRequestRemoteService} or
 * {@link AbstractMultiplePendingRequestsRemoteService}.
 *
 * <p>See {@code com.android.server.autofill.RemoteFillService} for a concrete
 * (no pun intended) example of how to use it.
 *
 * @param <S> the concrete remote service class
 * @param <I> the interface of the binder service
 *
 * @deprecated Use {@link ServiceConnector} to manage remote service connections
 *
 * @hide
 */
//TODO(b/117779333): improve javadoc above instead of using Autofill as an example
@Deprecated
public abstract class AbstractRemoteService<S extends AbstractRemoteService<S, I>,
        I extends IInterface> implements DeathRecipient {
    private static final int SERVICE_NOT_EXIST = -1;
    private static final int MSG_BIND = 1;
    private static final int MSG_UNBIND = 2;

    public static final long PERMANENT_BOUND_TIMEOUT_MS = 0;

    protected static final int LAST_PRIVATE_MSG = MSG_UNBIND;

    // TODO(b/117779333): convert all booleans into an integer / flags
    public final boolean mVerbose;

    protected final String mTag = getClass().getSimpleName();
    protected final Handler mHandler;
    protected final ComponentName mComponentName;

    private final Context mContext;
    private final Intent mIntent;
    private final VultureCallback<S> mVultureCallback;
    private final int mUserId;
    private final ServiceConnection mServiceConnection = new RemoteServiceConnection();
    private final int mBindingFlags;
    protected I mService;

    private boolean mBound;
    private boolean mConnecting;
    private boolean mDestroyed;
    private boolean mServiceDied;
    private boolean mCompleted;

    // Used just for debugging purposes (on dump)
    private long mNextUnbind;
    // Used just for debugging purposes (on dump)
    private int mServiceExitReason;
    private int mServiceExitSubReason;

    /** Requests that have been scheduled, but that are not finished yet */
    protected final ArrayList<BasePendingRequest<S, I>> mUnfinishedRequests = new ArrayList<>();

    /**
     * Callback called when the service dies.
     *
     * @param <T> service class
     */
    public interface VultureCallback<T> {
        /**
         * Called when the service dies.
         *
         * @param service service that died!
         */
        void onServiceDied(T service);
    }

    // NOTE: must be package-protected so this class is not extended outside
    AbstractRemoteService(@NonNull Context context, @NonNull String serviceInterface,
            @NonNull ComponentName componentName, int userId, @NonNull VultureCallback<S> callback,
            @NonNull Handler handler, int bindingFlags, boolean verbose) {
        mContext = context;
        mVultureCallback = callback;
        mVerbose = verbose;
        mComponentName = componentName;
        mIntent = new Intent(serviceInterface).setComponent(mComponentName);
        mUserId = userId;
        mHandler = new Handler(handler.getLooper());
        mBindingFlags = bindingFlags;
        mServiceExitReason = SERVICE_NOT_EXIST;
        mServiceExitSubReason = SERVICE_NOT_EXIST;
    }

    /**
     * Destroys this service.
     */
    public final void destroy() {
        mHandler.sendMessage(obtainMessage(AbstractRemoteService::handleDestroy, this));
    }

    /**
     * Checks whether this service is destroyed.
     */
    public final boolean isDestroyed() {
        return mDestroyed;
    }

    /**
     * Gets the name of the service.
     */
    @NonNull
    public final ComponentName getComponentName() {
        return mComponentName;
    }

    private void handleOnConnectedStateChangedInternal(boolean connected) {
        handleOnConnectedStateChanged(connected);
        if (connected) {
            handlePendingRequests();
        }
    }

    /**
     * Handles the pending requests when the connection it bounds to the remote service.
     */
    abstract void handlePendingRequests();

    /**
     * Callback called when the system connected / disconnected to the service and the pending
     * requests have been handled.
     *
     * @param state {@code true} when connected, {@code false} when disconnected.
     */
    protected void handleOnConnectedStateChanged(boolean state) {
    }

    /**
     * Gets the base Binder interface from the service.
     */
    @NonNull
    protected abstract I getServiceInterface(@NonNull IBinder service);

    /**
     * Defines how long after the last interaction with the service we would unbind.
     *
     * @return time to unbind (in millis), or {@link #PERMANENT_BOUND_TIMEOUT_MS} to not unbind.
     */
    protected abstract long getTimeoutIdleBindMillis();

    /**
     * Defines how long after we make a remote request to a fill service we timeout.
     *
     * <p>Just need to be overridden by subclasses that uses sync {@link PendingRequest}s.
     *
     * @throws UnsupportedOperationException if called when not overridden.
     *
     */
    protected long getRemoteRequestMillis() {
        throw new UnsupportedOperationException("not implemented by " + getClass());
    }

    /**
     * Gets the currently registered service interface or {@code null} if the service is not
     * connected.
     */
    @Nullable
    public final I getServiceInterface() {
        return mService;
    }

    private void handleDestroy() {
        if (checkIfDestroyed()) return;
        handleOnDestroy();
        handleEnsureUnbound();
        mDestroyed = true;
    }

    /**
     * Clears the state when this object is destroyed.
     *
     * <p>Typically used to cancel the pending requests.
     */
    protected abstract void handleOnDestroy();

    @Override // from DeathRecipient
    public void binderDied() {
        mHandler.sendMessage(obtainMessage(AbstractRemoteService::handleBinderDied, this));
    }

    private void handleBinderDied() {
        if (checkIfDestroyed()) return;
        if (mService != null) {
            mService.asBinder().unlinkToDeath(this, 0);
        }
        updateServicelicationExitInfo(mComponentName, mUserId);
        mConnecting = true;
        mService = null;
        mServiceDied = true;
        cancelScheduledUnbind();
        @SuppressWarnings("unchecked") // TODO(b/117779333): fix this warning
        final S castService = (S) this;
        mVultureCallback.onServiceDied(castService);
        handleBindFailure();
    }

    private void updateServicelicationExitInfo(ComponentName componentName, int userId) {
        IActivityManager am = ActivityManager.getService();
        String packageName = componentName.getPackageName();
        ParceledListSlice<ApplicationExitInfo> plistSlice = null;
        try {
            plistSlice = am.getHistoricalProcessExitReasons(packageName, 0, 1, userId);
        } catch (RemoteException e) {
            // do nothing. The local binder so it can not throw it.
        }
        if (plistSlice == null) {
            return;
        }
        List<ApplicationExitInfo> list = plistSlice.getList();
        if (list.isEmpty()) {
            return;
        }
        ApplicationExitInfo info = list.get(0);
        mServiceExitReason = info.getReason();
        mServiceExitSubReason = info.getSubReason();
        if (mVerbose) {
            Slog.v(mTag, "updateServicelicationExitInfo: exitReason="
                    + ApplicationExitInfo.reasonCodeToString(mServiceExitReason)
                    + " exitSubReason= " + ApplicationExitInfo.subreasonToString(
                    mServiceExitSubReason));
        }
    }

    // Note: we are dumping without a lock held so this is a bit racy but
    // adding a lock to a class that offloads to a handler thread would
    // mean adding a lock adding overhead to normal runtime operation.
    /**
     * Dump it!
     */
    public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
        String tab = "  ";
        pw.append(prefix).append("service:").println();
        pw.append(prefix).append(tab).append("userId=")
                .append(String.valueOf(mUserId)).println();
        pw.append(prefix).append(tab).append("componentName=")
                .append(mComponentName.flattenToString()).println();
        pw.append(prefix).append(tab).append("destroyed=")
                .append(String.valueOf(mDestroyed)).println();
        pw.append(prefix).append(tab).append("numUnfinishedRequests=")
                .append(String.valueOf(mUnfinishedRequests.size())).println();
        pw.append(prefix).append(tab).append("bound=")
                .append(String.valueOf(mBound));
        final boolean bound = handleIsBound();
        pw.append(prefix).append(tab).append("connected=")
                .append(String.valueOf(bound));
        final long idleTimeout = getTimeoutIdleBindMillis();
        if (bound) {
            if (idleTimeout > 0) {
                pw.append(" (unbind in : ");
                TimeUtils.formatDuration(mNextUnbind - SystemClock.elapsedRealtime(), pw);
                pw.append(")");
            } else {
                pw.append(" (permanently bound)");
            }
        }
        pw.println();
        if (mServiceExitReason != SERVICE_NOT_EXIST) {
            pw.append(prefix).append(tab).append("serviceExistReason=")
                    .append(ApplicationExitInfo.reasonCodeToString(mServiceExitReason));
            pw.println();
        }
        if (mServiceExitSubReason != SERVICE_NOT_EXIST) {
            pw.append(prefix).append(tab).append("serviceExistSubReason=")
                    .append(ApplicationExitInfo.subreasonToString(mServiceExitSubReason));
            pw.println();
        }
        pw.append(prefix).append("mBindingFlags=").println(mBindingFlags);
        pw.append(prefix).append("idleTimeout=")
            .append(Long.toString(idleTimeout / 1000)).append("s\n");
        pw.append(prefix).append("requestTimeout=");
        try {
            pw.append(Long.toString(getRemoteRequestMillis() / 1000)).append("s\n");
        } catch (UnsupportedOperationException e) {
            pw.append("not supported\n");
        }
        pw.println();
    }

    /**
     * Schedules a "sync" request.
     *
     * <p>This request must be responded by the service somehow (typically using a callback),
     * othewise it will trigger a {@link PendingRequest#onTimeout(AbstractRemoteService)} if the
     * service doesn't respond.
     */
    protected void scheduleRequest(@NonNull BasePendingRequest<S, I> pendingRequest) {
        mHandler.sendMessage(obtainMessage(
                AbstractRemoteService::handlePendingRequest, this, pendingRequest));
    }

    /**
     * Marks a pendingRequest as finished.
     *
     * @param finshedRequest The request that is finished
     */
    void finishRequest(@NonNull BasePendingRequest<S, I> finshedRequest) {
        mHandler.sendMessage(
                obtainMessage(AbstractRemoteService::handleFinishRequest, this, finshedRequest));
    }

    private void handleFinishRequest(@NonNull BasePendingRequest<S, I> finishedRequest) {
        synchronized (mUnfinishedRequests) {
            mUnfinishedRequests.remove(finishedRequest);
        }
        if (mUnfinishedRequests.isEmpty()) {
            scheduleUnbind();
        }
    }

    /**
     * Schedules an async request.
     *
     * <p>This request is not expecting a callback from the service, hence it's represented by
     * a simple {@link Runnable}.
     */
    protected void scheduleAsyncRequest(@NonNull AsyncRequest<I> request) {
        // TODO(b/117779333): fix generics below
        @SuppressWarnings({"unchecked", "rawtypes"})
        final MyAsyncPendingRequest<S, I> asyncRequest = new MyAsyncPendingRequest(this, request);
        mHandler.sendMessage(
                obtainMessage(AbstractRemoteService::handlePendingRequest, this, asyncRequest));
    }

    /**
     * Executes an async request immediately instead of sending it to Handler queue as what
     * {@link scheduleAsyncRequest} does.
     *
     * <p>This request is not expecting a callback from the service, hence it's represented by
     * a simple {@link Runnable}.
     */
    protected void executeAsyncRequest(@NonNull AsyncRequest<I> request) {
        // TODO(b/117779333): fix generics below
        @SuppressWarnings({"unchecked", "rawtypes"})
        final MyAsyncPendingRequest<S, I> asyncRequest = new MyAsyncPendingRequest(this, request);
        handlePendingRequest(asyncRequest);
    }

    private void cancelScheduledUnbind() {
        mHandler.removeMessages(MSG_UNBIND);
    }

    /**
     * Schedules a request to bind to the remote service.
     *
     * <p>Typically used on constructor for implementations that need a permanent connection to
     * the remote service.
     */
    protected void scheduleBind() {
        if (mHandler.hasMessages(MSG_BIND)) {
            if (mVerbose) Slog.v(mTag, "scheduleBind(): already scheduled");
            return;
        }
        mHandler.sendMessage(obtainMessage(AbstractRemoteService::handleEnsureBound, this)
                .setWhat(MSG_BIND));
    }

    /**
     * Schedules a request to automatically unbind from the service after the
     * {@link #getTimeoutIdleBindMillis() idle timeout} expires.
     */
    protected void scheduleUnbind() {
        scheduleUnbind(true);
    }

    private void scheduleUnbind(boolean delay) {
        long unbindDelay = getTimeoutIdleBindMillis();

        if (unbindDelay <= PERMANENT_BOUND_TIMEOUT_MS) {
            if (mVerbose) Slog.v(mTag, "not scheduling unbind when value is " + unbindDelay);
            return;
        }

        if (!delay) {
            unbindDelay = 0;
        }

        cancelScheduledUnbind();
        // TODO(b/117779333): make sure it's unbound if the service settings changing (right now
        // it's not)

        mNextUnbind = SystemClock.elapsedRealtime() + unbindDelay;
        if (mVerbose) Slog.v(mTag, "unbinding in " + unbindDelay + "ms: " + mNextUnbind);
        mHandler.sendMessageDelayed(obtainMessage(AbstractRemoteService::handleUnbind, this)
                .setWhat(MSG_UNBIND), unbindDelay);
    }

    private void handleUnbind() {
        if (checkIfDestroyed()) return;

        handleEnsureUnbound();
    }

    /**
     * Handles a request, either processing it right now when bound, or saving it to be handled when
     * bound.
     */
    protected final void handlePendingRequest(@NonNull BasePendingRequest<S, I> pendingRequest) {
        if (checkIfDestroyed() || mCompleted) return;

        if (!handleIsBound()) {
            if (mVerbose) Slog.v(mTag, "handlePendingRequest(): queuing " + pendingRequest);
            handlePendingRequestWhileUnBound(pendingRequest);
            handleEnsureBound();
        } else {
            if (mVerbose) Slog.v(mTag, "handlePendingRequest(): " + pendingRequest);

            synchronized (mUnfinishedRequests) {
                mUnfinishedRequests.add(pendingRequest);
            }
            cancelScheduledUnbind();

            pendingRequest.run();
            if (pendingRequest.isFinal()) {
                mCompleted = true;
            }
        }
    }

    /**
     * Defines what to do with a request that arrives while not bound to the service.
     */
    abstract void handlePendingRequestWhileUnBound(
            @NonNull BasePendingRequest<S, I> pendingRequest);

    /**
     * Called if {@link Context#bindServiceAsUser} returns {@code false}, or
     * if {@link DeathRecipient#binderDied()} is called.
     */
    abstract void handleBindFailure();

    // This is actually checking isConnected. TODO: rename this and other related methods (or just
    // stop using this class..)
    private boolean handleIsBound() {
        return mService != null;
    }

    private void handleEnsureBound() {
        if (handleIsBound() || mConnecting) return;

        if (mVerbose) Slog.v(mTag, "ensureBound()");
        mConnecting = true;

        final int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
                | Context.BIND_INCLUDE_CAPABILITIES | mBindingFlags;

        final boolean willBind = mContext.bindServiceAsUser(mIntent, mServiceConnection, flags,
                mHandler, new UserHandle(mUserId));
        mBound = true;

        if (!willBind) {
            Slog.w(mTag, "could not bind to " + mIntent + " using flags " + flags);
            mConnecting = false;

            if (!mServiceDied) {
                handleBinderDied();
            }
        }
    }

    private void handleEnsureUnbound() {
        if (!handleIsBound() && !mConnecting) return;

        if (mVerbose) Slog.v(mTag, "ensureUnbound()");
        mConnecting = false;
        if (handleIsBound()) {
            handleOnConnectedStateChangedInternal(false);
            if (mService != null) {
                mService.asBinder().unlinkToDeath(this, 0);
                mService = null;
            }
        }
        mNextUnbind = 0;
        if (mBound) {
            mContext.unbindService(mServiceConnection);
            mBound = false;
        }
    }

    private class RemoteServiceConnection implements ServiceConnection {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            if (mVerbose) Slog.v(mTag, "onServiceConnected()");
            if (mDestroyed || !mConnecting) {
                // This is abnormal. Unbinding the connection has been requested already.
                Slog.wtf(mTag, "onServiceConnected() was dispatched after unbindService.");
                return;
            }
            mConnecting = false;
            try {
                service.linkToDeath(AbstractRemoteService.this, 0);
            } catch (RemoteException re) {
                handleBinderDied();
                return;
            }
            mService = getServiceInterface(service);
            mServiceExitReason = SERVICE_NOT_EXIST;
            mServiceExitSubReason = SERVICE_NOT_EXIST;
            handleOnConnectedStateChangedInternal(true);
            mServiceDied = false;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            if (mVerbose) Slog.v(mTag, "onServiceDisconnected()");
            mConnecting = true;
            mService = null;
        }

        @Override
        public void onBindingDied(ComponentName name) {
            if (mVerbose) Slog.v(mTag, "onBindingDied()");
            scheduleUnbind(false);
        }
    }

    private boolean checkIfDestroyed() {
        if (mDestroyed) {
            if (mVerbose) {
                Slog.v(mTag, "Not handling operation as service for " + mComponentName
                        + " is already destroyed");
            }
        }
        return mDestroyed;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + mComponentName
                + " " + System.identityHashCode(this)
                + (mService != null ? " (bound)" : " (unbound)")
                + (mDestroyed ? " (destroyed)" : "")
                + "]";
    }

    /**
     * Base class for the requests serviced by the remote service.
     *
     * <p><b>NOTE: </b> this class is not used directly, you should either override
     * {@link com.android.internal.infra.AbstractRemoteService.PendingRequest} for sync requests, or
     * use {@link AbstractRemoteService#scheduleAsyncRequest(AsyncRequest)} for async requests.
     *
     * @param <S> the remote service class
     * @param <I> the interface of the binder service
     */
    public abstract static class BasePendingRequest<S extends AbstractRemoteService<S, I>,
            I extends IInterface> implements Runnable {
        protected final String mTag = getClass().getSimpleName();
        protected final Object mLock = new Object();

        final WeakReference<S> mWeakService;

        @GuardedBy("mLock")
        boolean mCancelled;

        @GuardedBy("mLock")
        boolean mCompleted;

        BasePendingRequest(@NonNull S service) {
            mWeakService = new WeakReference<>(service);
        }

        /**
         * Gets a reference to the remote service.
         */
        protected final S getService() {
            return mWeakService.get();
        }

        /**
         * Subclasses must call this method when the remote service finishes, i.e., when the service
         * finishes processing a request.
         *
         * @return {@code false} in the service is already finished, {@code true} otherwise.
         */
        protected final boolean finish() {
            synchronized (mLock) {
                if (mCompleted || mCancelled) {
                    return false;
                }
                mCompleted = true;
            }

            S service = mWeakService.get();
            if (service != null) {
                service.finishRequest(this);
            }

            onFinished();

            return true;
        }

        void onFinished() { }

        /**
         * Called when request fails due to reasons internal to {@link AbstractRemoteService},
         * e.g. failure to bind to service.
         */
        protected void onFailed() { }

        /**
         * Checks whether this request was cancelled.
         */
        @GuardedBy("mLock")
        protected final boolean isCancelledLocked() {
            return mCancelled;
        }

        /**
         * Cancels the service.
         *
         * @return {@code false} if service is already canceled, {@code true} otherwise.
         */
        public boolean cancel() {
            synchronized (mLock) {
                if (mCancelled || mCompleted) {
                    return false;
                }
                mCancelled = true;
            }

            S service = mWeakService.get();
            if (service != null) {
                service.finishRequest(this);
            }

            onCancel();
            return true;
        }

        void onCancel() {}

        /**
         * Checks whether this request leads to a final state where no other requests can be made.
         */
        protected boolean isFinal() {
            return false;
        }

        protected boolean isRequestCompleted() {
            synchronized (mLock) {
                return mCompleted;
            }
        }
    }

    /**
     * Base class for the requests serviced by the remote service.
     *
     * <p><b>NOTE: </b> this class is typically used when the service needs to use a callback to
     * communicate back with the system server. For cases where that's not needed, you should use
     * {@link AbstractRemoteService#scheduleAsyncRequest(AsyncRequest)} instead.
     *
     * <p><b>NOTE: </b> you must override {@link AbstractRemoteService#getRemoteRequestMillis()},
     * otherwise the constructor will throw an {@link UnsupportedOperationException}.
     *
     * @param <S> the remote service class
     * @param <I> the interface of the binder service
     */
    public abstract static class PendingRequest<S extends AbstractRemoteService<S, I>,
            I extends IInterface> extends BasePendingRequest<S, I> {

        private final Runnable mTimeoutTrigger;
        private final Handler mServiceHandler;

        protected PendingRequest(S service) {
            super(service);
            mServiceHandler = service.mHandler;

            mTimeoutTrigger = () -> {
                synchronized (mLock) {
                    if (mCancelled) {
                        return;
                    }
                    mCompleted = true;
                }

                final S remoteService = mWeakService.get();
                if (remoteService != null) {
                    // TODO(b/117779333): we should probably ignore it if service is destroyed.
                    Slog.w(mTag, "timed out after " + service.getRemoteRequestMillis() + " ms");
                    remoteService.finishRequest(this);
                    onTimeout(remoteService);
                } else {
                    Slog.w(mTag, "timed out (no service)");
                }
            };
            mServiceHandler.postAtTime(mTimeoutTrigger,
                    SystemClock.uptimeMillis() + service.getRemoteRequestMillis());
        }

        @Override
        final void onFinished() {
            mServiceHandler.removeCallbacks(mTimeoutTrigger);
        }

        @Override
        final void onCancel() {
            mServiceHandler.removeCallbacks(mTimeoutTrigger);
        }

        /**
         * Called by the self-destruct timeout when the remote service didn't reply to the
         * request on time.
         */
        protected abstract void onTimeout(S remoteService);
    }

    /**
     * Represents a request that does not expect a callback from the remote service.
     *
     * @param <I> the interface of the binder service
     */
    public interface AsyncRequest<I extends IInterface> {

        /**
         * Run Forrest, run!
         */
        void run(@NonNull I binder) throws RemoteException;
    }

    private static final class MyAsyncPendingRequest<S extends AbstractRemoteService<S, I>,
            I extends IInterface> extends BasePendingRequest<S, I> {
        private static final String TAG = MyAsyncPendingRequest.class.getSimpleName();

        private final AsyncRequest<I> mRequest;

        protected MyAsyncPendingRequest(@NonNull S service, @NonNull AsyncRequest<I> request) {
            super(service);

            mRequest = request;
        }

        @Override
        public void run() {
            final S remoteService = getService();
            if (remoteService == null) return;
            try {
                mRequest.run(remoteService.mService);
            } catch (RemoteException e) {
                Slog.w(TAG, "exception handling async request (" + this + "): " + e);
            } finally {
                finish();
            }
        }
    }
}
