/**
 * Copyright (c) 2016, 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.utils;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
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 java.text.SimpleDateFormat;
import java.util.Objects;
import java.util.Date;

/**
 * Manages the lifecycle of an application-provided service bound from system server.
 *
 * @hide
 */
public class ManagedApplicationService {
    private final String TAG = getClass().getSimpleName();

    /**
     * Attempt to reconnect service forever if an onBindingDied or onServiceDisconnected event
     * is received.
     */
    public static final int RETRY_FOREVER = 1;

    /**
     * Never attempt to reconnect the service - a single onBindingDied or onServiceDisconnected
     * event will cause this to fully unbind the service and never attempt to reconnect.
     */
    public static final int RETRY_NEVER = 2;

    /**
     * Attempt to reconnect the service until the maximum number of retries is reached, then stop.
     *
     * The first retry will occur MIN_RETRY_DURATION_MS after the disconnection, and each
     * subsequent retry will occur after 2x the duration used for the previous retry up to the
     * MAX_RETRY_DURATION_MS duration.
     *
     * In this case, retries mean a full unbindService/bindService pair to handle cases when the
     * usual service re-connection logic in ActiveServices has very high backoff times or when the
     * serviceconnection has fully died due to a package update or similar.
     */
    public static final int RETRY_BEST_EFFORT = 3;

    // Maximum number of retries before giving up (for RETRY_BEST_EFFORT).
    private static final int MAX_RETRY_COUNT = 4;
    // Max time between retry attempts.
    private static final long MAX_RETRY_DURATION_MS = 16000;
    // Min time between retry attempts.
    private static final long MIN_RETRY_DURATION_MS = 2000;
    // Time since the last retry attempt after which to clear the retry attempt counter.
    private static final long RETRY_RESET_TIME_MS = MAX_RETRY_DURATION_MS * 4;

    private final Context mContext;
    private final int mUserId;
    private final ComponentName mComponent;
    private final int mClientLabel;
    private final String mSettingsAction;
    private final BinderChecker mChecker;
    private final boolean mIsImportant;
    private final int mRetryType;
    private final Handler mHandler;
    private final Runnable mRetryRunnable = this::doRetry;
    private final EventCallback mEventCb;

    private final Object mLock = new Object();

    // State protected by mLock
    private ServiceConnection mConnection;
    private IInterface mBoundInterface;
    private PendingEvent mPendingEvent;
    private int mRetryCount;
    private long mLastRetryTimeMs;
    private long mNextRetryDurationMs = MIN_RETRY_DURATION_MS;
    private boolean mRetrying;

    public static interface LogFormattable {
       String toLogString(SimpleDateFormat dateFormat);
    }

    /**
     * Lifecycle event of this managed service.
     */
    public static class LogEvent implements LogFormattable {
        public static final int EVENT_CONNECTED = 1;
        public static final int EVENT_DISCONNECTED = 2;
        public static final int EVENT_BINDING_DIED = 3;
        public static final int EVENT_STOPPED_PERMANENTLY = 4;

        // Time of the events in "current time ms" timebase.
        public final long timestamp;
        // Name of the component for this system service.
        public final ComponentName component;
        // ID of the event that occurred.
        public final int event;

        public LogEvent(long timestamp, ComponentName component, int event) {
            this.timestamp = timestamp;
            this.component = component;
            this.event = event;
        }

        @Override
        public String toLogString(SimpleDateFormat dateFormat) {
            return dateFormat.format(new Date(timestamp)) + "   " + eventToString(event)
                    + " Managed Service: "
                    + ((component == null) ? "None" : component.flattenToString());
        }

        public static String eventToString(int event) {
            switch (event) {
                case EVENT_CONNECTED:
                    return "Connected";
                case EVENT_DISCONNECTED:
                    return "Disconnected";
                case EVENT_BINDING_DIED:
                    return "Binding Died For";
                case EVENT_STOPPED_PERMANENTLY:
                    return "Permanently Stopped";
                default:
                    return "Unknown Event Occurred";
            }
        }
    }

    private ManagedApplicationService(final Context context, final ComponentName component,
            final int userId, int clientLabel, String settingsAction,
            BinderChecker binderChecker, boolean isImportant, int retryType, Handler handler,
            EventCallback eventCallback) {
        mContext = context;
        mComponent = component;
        mUserId = userId;
        mClientLabel = clientLabel;
        mSettingsAction = settingsAction;
        mChecker = binderChecker;
        mIsImportant = isImportant;
        mRetryType = retryType;
        mHandler = handler;
        mEventCb = eventCallback;
    }

    /**
     * Implement to validate returned IBinder instance.
     */
    public interface BinderChecker {
        IInterface asInterface(IBinder binder);
        boolean checkType(IInterface service);
    }

    /**
     * Implement to call IInterface methods after service is connected.
     */
    public interface PendingEvent {
        void runEvent(IInterface service) throws RemoteException;
    }

    /**
     * Implement to be notified about any problems with remote service.
     */
    public interface EventCallback {
        /**
         * Called when an sevice lifecycle event occurs.
         */
        void onServiceEvent(LogEvent event);
    }

    /**
     * Create a new ManagedApplicationService object but do not yet bind to the user service.
     *
     * @param context a Context to use for binding the application service.
     * @param component the {@link ComponentName} of the application service to bind.
     * @param userId the user ID of user to bind the application service as.
     * @param clientLabel the resource ID of a label displayed to the user indicating the
     *      binding service, or 0 if none is desired.
     * @param settingsAction an action that can be used to open the Settings UI to enable/disable
     *      binding to these services, or null if none is desired.
     * @param binderChecker an interface used to validate the returned binder object, or null if
     *      this interface is unchecked.
     * @param isImportant bind the user service with BIND_IMPORTANT.
     * @param retryType reconnect behavior to have when bound service is disconnected.
     * @param handler the Handler to use for retries and delivering EventCallbacks.
     * @param eventCallback a callback used to deliver disconnection events, or null if you
     *      don't care.
     * @return a ManagedApplicationService instance.
     */
    public static ManagedApplicationService build(@NonNull final Context context,
            @NonNull final ComponentName component, final int userId, int clientLabel,
            @Nullable String settingsAction, @Nullable BinderChecker binderChecker,
            boolean isImportant, int retryType, @NonNull Handler handler,
            @Nullable EventCallback eventCallback) {
        return new ManagedApplicationService(context, component, userId, clientLabel,
            settingsAction, binderChecker, isImportant, retryType, handler, eventCallback);
    }


    /**
     * @return the user ID of the user that owns the bound service.
     */
    public int getUserId() {
        return mUserId;
    }

    /**
     * @return the component of the bound service.
     */
    public ComponentName getComponent() {
        return mComponent;
    }

    /**
     * Asynchronously unbind from the application service if the bound service component and user
     * does not match the given signature.
     *
     * @param componentName the component that must match.
     * @param userId the user ID that must match.
     * @return {@code true} if not matching.
     */
    public boolean disconnectIfNotMatching(final ComponentName componentName, final int userId) {
        if (matches(componentName, userId)) {
            return false;
        }
        disconnect();
        return true;
    }

    /**
     * Send an event to run as soon as the binder interface is available.
     *
     * @param event a {@link PendingEvent} to send.
     */
    public void sendEvent(@NonNull PendingEvent event) {
        IInterface iface;
        synchronized (mLock) {
            iface = mBoundInterface;
            if (iface == null) {
                mPendingEvent = event;
            }
        }

        if (iface != null) {
            try {
                event.runEvent(iface);
            } catch (RuntimeException | RemoteException ex) {
                Slog.e(TAG, "Received exception from user service: ", ex);
            }
        }
    }

    /**
     * Asynchronously unbind from the application service if bound.
     */
    public void disconnect() {
        synchronized (mLock) {
            // Unbind existing connection, if it exists
            if (mConnection == null) {
                return;
            }

            mContext.unbindService(mConnection);
            mConnection = null;
            mBoundInterface = null;
        }
    }

    /**
     * Asynchronously bind to the application service if not bound.
     */
    public void connect() {
        synchronized (mLock) {
            if (mConnection != null) {
                // We're already connected or are trying to connect
                return;
            }

            Intent intent  = new Intent().setComponent(mComponent);
            if (mClientLabel != 0) {
                intent.putExtra(Intent.EXTRA_CLIENT_LABEL, mClientLabel);
            }
            if (mSettingsAction != null) {
                intent.putExtra(Intent.EXTRA_CLIENT_INTENT,
                        PendingIntent.getActivity(mContext, 0, new Intent(mSettingsAction), 0));
            }

            mConnection = new ServiceConnection() {
                @Override
                public void onBindingDied(ComponentName componentName) {
                    final long timestamp = System.currentTimeMillis();
                    Slog.w(TAG, "Service binding died: " + componentName);
                    synchronized (mLock) {
                        if (mConnection != this) {
                            return;
                        }
                        mHandler.post(() -> {
                            mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
                                  LogEvent.EVENT_BINDING_DIED));
                        });

                        mBoundInterface = null;
                        startRetriesLocked();
                    }
                }

                @Override
                public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                    final long timestamp = System.currentTimeMillis();
                    Slog.i(TAG, "Service connected: " + componentName);
                    IInterface iface = null;
                    PendingEvent pendingEvent = null;
                    synchronized (mLock) {
                        if (mConnection != this) {
                            // Must've been unbound.
                            return;
                        }
                        mHandler.post(() -> {
                            mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
                                  LogEvent.EVENT_CONNECTED));
                        });

                        stopRetriesLocked();

                        mBoundInterface = null;
                        if (mChecker != null) {
                            mBoundInterface = mChecker.asInterface(iBinder);
                            if (!mChecker.checkType(mBoundInterface)) {
                                // Received an invalid binder, disconnect.
                                mBoundInterface = null;
                                Slog.w(TAG, "Invalid binder from " + componentName);
                                startRetriesLocked();
                                return;
                            }
                            iface = mBoundInterface;
                            pendingEvent = mPendingEvent;
                            mPendingEvent = null;
                        }
                    }
                    if (iface != null && pendingEvent != null) {
                        try {
                            pendingEvent.runEvent(iface);
                        } catch (RuntimeException | RemoteException ex) {
                            Slog.e(TAG, "Received exception from user service: ", ex);
                            startRetriesLocked();
                        }
                    }
                }

                @Override
                public void onServiceDisconnected(ComponentName componentName) {
                    final long timestamp = System.currentTimeMillis();
                    Slog.w(TAG, "Service disconnected: " + componentName);
                    synchronized (mLock) {
                        if (mConnection != this) {
                            return;
                        }

                        mHandler.post(() -> {
                            mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
                                  LogEvent.EVENT_DISCONNECTED));
                        });

                        mBoundInterface = null;
                        startRetriesLocked();
                    }
                }
            };

            int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
            if (mIsImportant) {
                flags |= Context.BIND_IMPORTANT;
            }
            try {
                if (!mContext.bindServiceAsUser(intent, mConnection, flags,
                        new UserHandle(mUserId))) {
                    Slog.w(TAG, "Unable to bind service: " + intent);
                    startRetriesLocked();
                }
            } catch (SecurityException e) {
                Slog.w(TAG, "Unable to bind service: " + intent, e);
                startRetriesLocked();
            }
        }
    }

    private boolean matches(final ComponentName component, final int userId) {
        return Objects.equals(mComponent, component) && mUserId == userId;
    }

    private void startRetriesLocked() {
        if (checkAndDeliverServiceDiedCbLocked()) {
            // If we delivered the service callback, disconnect and stop retrying.
            disconnect();
            return;
        }

        if (mRetrying) {
            // Retry already queued, don't queue a new one.
            return;
        }
        mRetrying = true;
        queueRetryLocked();
    }

    private void stopRetriesLocked() {
        mRetrying = false;
        mHandler.removeCallbacks(mRetryRunnable);
    }

    private void queueRetryLocked() {
        long now = SystemClock.uptimeMillis();
        if ((now - mLastRetryTimeMs) > RETRY_RESET_TIME_MS) {
            // It's been longer than the reset time since we last had to retry.  Re-initialize.
            mNextRetryDurationMs = MIN_RETRY_DURATION_MS;
            mRetryCount = 0;
        }
        mLastRetryTimeMs = now;
        mHandler.postDelayed(mRetryRunnable, mNextRetryDurationMs);
        mNextRetryDurationMs = Math.min(2 * mNextRetryDurationMs, MAX_RETRY_DURATION_MS);
        mRetryCount++;
    }

    private boolean checkAndDeliverServiceDiedCbLocked() {

       if (mRetryType == RETRY_NEVER || (mRetryType == RETRY_BEST_EFFORT
                && mRetryCount >= MAX_RETRY_COUNT)) {
            // If we never retry, or we've exhausted our retries, post the onServiceDied callback.
            Slog.e(TAG, "Service " + mComponent + " has died too much, not retrying.");
            if (mEventCb != null) {
                final long timestamp = System.currentTimeMillis();
                mHandler.post(() -> {
                  mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
                        LogEvent.EVENT_STOPPED_PERMANENTLY));
                });
            }
            return true;
        }
        return false;
    }

    private void doRetry() {
        synchronized (mLock) {
            if (mConnection == null) {
                // We disconnected for good.  Don't attempt to retry.
                return;
            }
            if (!mRetrying) {
                // We successfully connected.  Don't attempt to retry.
                return;
            }
            Slog.i(TAG, "Attempting to reconnect " + mComponent + "...");
            // While frameworks may restart the remote Service if we stay bound, we have little
            // control of the backoff timing for reconnecting the service.  In the event of a
            // process crash, the backoff time can be very large (1-30 min), which is not
            // acceptable for the types of services this is used for.  Instead force an unbind/bind
            // sequence to cause a more immediate retry.
            disconnect();
            if (checkAndDeliverServiceDiedCbLocked()) {
                // No more retries.
                return;
            }
            queueRetryLocked();
            connect();
        }
    }
}
