/**
 * 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.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.IInterface;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;

import java.util.Objects;

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

    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 DeathRecipient mDeathRecipient = new DeathRecipient() {
        @Override
        public void binderDied() {
            synchronized (mLock) {
                mBoundInterface = null;
            }
        }
    };

    private final Object mLock = new Object();

    // State protected by mLock
    private ServiceConnection mPendingConnection;
    private ServiceConnection mConnection;
    private IInterface mBoundInterface;


    private ManagedApplicationService(final Context context, final ComponentName component,
            final int userId, int clientLabel, String settingsAction,
            BinderChecker binderChecker) {
        mContext = context;
        mComponent = component;
        mUserId = userId;
        mClientLabel = clientLabel;
        mSettingsAction = settingsAction;
        mChecker = binderChecker;
    }

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

    /**
     * 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.
     * @param settingsAction an action that can be used to open the Settings UI to enable/disable
     *      binding to these services.
     * @param binderChecker an interface used to validate the returned binder object.
     * @return a ManagedApplicationService instance.
     */
    public static ManagedApplicationService build(@NonNull final Context context,
        @NonNull final ComponentName component, final int userId, @NonNull int clientLabel,
        @NonNull String settingsAction, @NonNull BinderChecker binderChecker) {
        return new ManagedApplicationService(context, component, userId, clientLabel,
            settingsAction, binderChecker);
    }

    /**
     * @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;
    }

    /**
     * Asynchronously unbind from the application service if bound.
     */
    public void disconnect() {
        synchronized (mLock) {
            // Wipe out pending connections
            mPendingConnection = null;

            // Unbind existing connection, if it exists
            if (mConnection != null) {
                mContext.unbindService(mConnection);
                mConnection = null;
            }

            mBoundInterface = null;
        }
    }

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

            final PendingIntent pendingIntent = PendingIntent.getActivity(
                    mContext, 0, new Intent(mSettingsAction), 0);
            final Intent intent = new Intent().setComponent(mComponent).
                    putExtra(Intent.EXTRA_CLIENT_LABEL, mClientLabel).
                    putExtra(Intent.EXTRA_CLIENT_INTENT, pendingIntent);

            final ServiceConnection serviceConnection = new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                    synchronized (mLock) {
                        if (mPendingConnection == this) {
                            // No longer pending, remove from pending connection
                            mPendingConnection = null;
                            mConnection = this;
                        } else {
                            // Service connection wasn't pending, must have been disconnected
                            mContext.unbindService(this);
                        }

                        try {
                            iBinder.linkToDeath(mDeathRecipient, 0);
                            mBoundInterface = mChecker.asInterface(iBinder);
                            if (!mChecker.checkType(mBoundInterface)) {
                                // Received an invalid binder, disconnect
                                mContext.unbindService(this);
                                mBoundInterface = null;
                            }
                        } catch (RemoteException e) {
                            // DOA
                            Slog.w(TAG, "Unable to bind service: " + intent, e);
                            mBoundInterface = null;
                        }
                    }
                }

                @Override
                public void onServiceDisconnected(ComponentName componentName) {
                    Slog.w(TAG, "Service disconnected: " + intent);
                }
            };

            mPendingConnection = serviceConnection;

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

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