/*
 * Copyright (C) 2012 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;

import static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Context.BIND_NOT_FOREGROUND;
import static android.content.Context.BIND_NOT_VISIBLE;
import static android.content.pm.PackageManager.GET_META_DATA;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;

import android.annotation.BoolRes;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;

import com.android.internal.content.PackageMonitor;
import com.android.internal.util.Preconditions;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Maintains a binding to the best service that matches the given intent information. Bind and
 * unbind callbacks, as well as all binder operations, will all be run on the given handler.
 */
public class ServiceWatcher implements ServiceConnection {

    private static final String TAG = "ServiceWatcher";
    private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);

    private static final String EXTRA_SERVICE_VERSION = "serviceVersion";
    private static final String EXTRA_SERVICE_IS_MULTIUSER = "serviceIsMultiuser";

    private static final long BLOCKING_BINDER_TIMEOUT_MS = 30 * 1000;

    /** Function to run on binder interface. */
    public interface BinderRunner {
        /** Called to run client code with the binder. */
        void run(IBinder binder) throws RemoteException;
    }

    /**
     * Function to run on binder interface.
     * @param <T> Type to return.
     */
    public interface BlockingBinderRunner<T> {
        /** Called to run client code with the binder. */
        T run(IBinder binder) throws RemoteException;
    }

    /**
     * Information on the service ServiceWatcher has selected as the best option for binding.
     */
    public static final class ServiceInfo implements Comparable<ServiceInfo> {

        public static final ServiceInfo NONE = new ServiceInfo(Integer.MIN_VALUE, null,
                UserHandle.USER_NULL);

        public final int version;
        @Nullable public final ComponentName component;
        @UserIdInt public final int userId;

        ServiceInfo(ResolveInfo resolveInfo, int currentUserId) {
            Preconditions.checkArgument(resolveInfo.serviceInfo.getComponentName() != null);

            Bundle metadata = resolveInfo.serviceInfo.metaData;
            boolean isMultiuser;
            if (metadata != null) {
                version = metadata.getInt(EXTRA_SERVICE_VERSION, Integer.MIN_VALUE);
                isMultiuser = metadata.getBoolean(EXTRA_SERVICE_IS_MULTIUSER, false);
            } else {
                version = Integer.MIN_VALUE;
                isMultiuser = false;
            }

            component = resolveInfo.serviceInfo.getComponentName();
            userId = isMultiuser ? UserHandle.USER_SYSTEM : currentUserId;
        }

        private ServiceInfo(int version, @Nullable ComponentName component, int userId) {
            Preconditions.checkArgument(component != null || version == Integer.MIN_VALUE);
            this.version = version;
            this.component = component;
            this.userId = userId;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof ServiceInfo)) {
                return false;
            }
            ServiceInfo that = (ServiceInfo) o;
            return version == that.version && userId == that.userId
                    && Objects.equals(component, that.component);
        }

        @Override
        public int hashCode() {
            return Objects.hash(version, component, userId);
        }

        @Override
        public int compareTo(ServiceInfo that) {
            // ServiceInfos with higher version numbers always win (having a version number >
            // MIN_VALUE implies having a non-null component). if version numbers are equal, a
            // non-null component wins over a null component. if the version numbers are equal and
            // both components exist then we prefer components that work for all users vs components
            // that only work for a single user at a time. otherwise everything's equal.
            int ret = Integer.compare(version, that.version);
            if (ret == 0) {
                if (component == null && that.component != null) {
                    ret = -1;
                } else if (component != null && that.component == null) {
                    ret = 1;
                } else {
                    if (userId != UserHandle.USER_SYSTEM && that.userId == UserHandle.USER_SYSTEM) {
                        ret = -1;
                    } else if (userId == UserHandle.USER_SYSTEM
                            && that.userId != UserHandle.USER_SYSTEM) {
                        ret = 1;
                    }
                }
            }
            return ret;
        }

        @Override
        public String toString() {
            if (component == null) {
                return "none";
            } else {
                return component.toShortString() + "@" + version + "[u" + userId + "]";
            }
        }
    }

    private final Context mContext;
    private final Handler mHandler;
    private final Intent mIntent;

    @Nullable private final BinderRunner mOnBind;
    @Nullable private final Runnable mOnUnbind;

    // read/write from handler thread only
    private int mCurrentUserId;

    // write from handler thread only, read anywhere
    private volatile ServiceInfo mServiceInfo;
    private volatile IBinder mBinder;

    public ServiceWatcher(Context context, Handler handler, String action,
            @Nullable BinderRunner onBind, @Nullable Runnable onUnbind,
            @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) {
        mContext = context;
        mHandler = FgThread.getHandler();
        mIntent = new Intent(Objects.requireNonNull(action));

        Resources resources = context.getResources();
        boolean enableOverlay = resources.getBoolean(enableOverlayResId);
        if (!enableOverlay) {
            mIntent.setPackage(resources.getString(nonOverlayPackageResId));
        }

        mOnBind = onBind;
        mOnUnbind = onUnbind;

        mCurrentUserId = UserHandle.USER_NULL;

        mServiceInfo = ServiceInfo.NONE;
        mBinder = null;
    }

    /**
     * Register this class, which will start the process of determining the best matching service
     * and maintaining a binding to it. Will return false and fail if there are no possible matching
     * services at the time this functions is called.
     */
    public boolean register() {
        if (mContext.getPackageManager().queryIntentServicesAsUser(mIntent,
                MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_SYSTEM_ONLY,
                UserHandle.USER_SYSTEM).isEmpty()) {
            return false;
        }

        new PackageMonitor() {
            @Override
            public void onPackageUpdateFinished(String packageName, int uid) {
                ServiceWatcher.this.onPackageChanged(packageName);
            }

            @Override
            public void onPackageAdded(String packageName, int uid) {
                ServiceWatcher.this.onPackageChanged(packageName);
            }

            @Override
            public void onPackageRemoved(String packageName, int uid) {
                ServiceWatcher.this.onPackageChanged(packageName);
            }

            @Override
            public boolean onPackageChanged(String packageName, int uid, String[] components) {
                ServiceWatcher.this.onPackageChanged(packageName);
                return super.onPackageChanged(packageName, uid, components);
            }
        }.register(mContext, UserHandle.ALL, true, mHandler);

        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
        intentFilter.addAction(Intent.ACTION_USER_UNLOCKED);
        mContext.registerReceiverAsUser(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (action == null) {
                    return;
                }
                int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
                if (userId == UserHandle.USER_NULL) {
                    return;
                }

                switch (action) {
                    case Intent.ACTION_USER_SWITCHED:
                        onUserSwitched(userId);
                        break;
                    case Intent.ACTION_USER_UNLOCKED:
                        onUserUnlocked(userId);
                        break;
                    default:
                        break;
                }

            }
        }, UserHandle.ALL, intentFilter, null, mHandler);

        mCurrentUserId = ActivityManager.getCurrentUser();

        mHandler.post(() -> onBestServiceChanged(false));
        return true;
    }

    /**
     * Returns information on the currently selected service.
     */
    public ServiceInfo getBoundService() {
        return mServiceInfo;
    }

    private void onBestServiceChanged(boolean forceRebind) {
        Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());

        List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
                mIntent,
                GET_META_DATA | MATCH_DIRECT_BOOT_AUTO | MATCH_SYSTEM_ONLY,
                mCurrentUserId);

        ServiceInfo bestServiceInfo = ServiceInfo.NONE;
        for (ResolveInfo resolveInfo : resolveInfos) {
            ServiceInfo serviceInfo = new ServiceInfo(resolveInfo, mCurrentUserId);
            if (serviceInfo.compareTo(bestServiceInfo) > 0) {
                bestServiceInfo = serviceInfo;
            }
        }

        if (forceRebind || !bestServiceInfo.equals(mServiceInfo)) {
            rebind(bestServiceInfo);
        }
    }

    private void rebind(ServiceInfo newServiceInfo) {
        Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());

        if (!mServiceInfo.equals(ServiceInfo.NONE)) {
            if (D) {
                Log.i(TAG, "[" + mIntent.getAction() + "] unbinding from " + mServiceInfo);
            }

            mContext.unbindService(this);
            onServiceDisconnected(mServiceInfo.component);
            mServiceInfo = ServiceInfo.NONE;
        }

        mServiceInfo = newServiceInfo;
        if (mServiceInfo.equals(ServiceInfo.NONE)) {
            return;
        }

        Preconditions.checkState(mServiceInfo.component != null);

        if (D) {
            Log.i(TAG, getLogPrefix() + " binding to " + mServiceInfo);
        }

        Intent bindIntent = new Intent(mIntent).setComponent(mServiceInfo.component);
        mContext.bindServiceAsUser(bindIntent, this,
                BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE,
                mHandler, UserHandle.of(mServiceInfo.userId));
    }

    @Override
    public final void onServiceConnected(ComponentName component, IBinder binder) {
        Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
        Preconditions.checkState(mBinder == null);

        if (D) {
            Log.i(TAG, getLogPrefix() + " connected to " + component.toShortString());
        }

        mBinder = binder;
        if (mOnBind != null) {
            try {
                mOnBind.run(binder);
            } catch (RuntimeException | RemoteException e) {
                // binders may propagate some specific non-RemoteExceptions from the other side
                // through the binder as well - we cannot allow those to crash the system server
                Log.e(TAG, getLogPrefix() + " exception running on " + mServiceInfo, e);
            }
        }
    }

    @Override
    public final void onServiceDisconnected(ComponentName component) {
        Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());

        if (mBinder == null) {
            return;
        }

        if (D) {
            Log.i(TAG, getLogPrefix() + " disconnected from " + component.toShortString());
        }

        mBinder = null;
        if (mOnUnbind != null) {
            mOnUnbind.run();
        }
    }

    @Override
    public void onBindingDied(ComponentName component) {
        Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());

        if (D) {
            Log.i(TAG, getLogPrefix() + " " + component.toShortString() + " died");
        }

        onBestServiceChanged(true);
    }

    void onUserSwitched(@UserIdInt int userId) {
        mCurrentUserId = userId;
        onBestServiceChanged(false);
    }

    void onUserUnlocked(@UserIdInt int userId) {
        if (userId == mCurrentUserId) {
            onBestServiceChanged(false);
        }
    }

    void onPackageChanged(String packageName) {
        // force a rebind if the changed package was the currently connected package
        String currentPackageName =
                mServiceInfo.component != null ? mServiceInfo.component.getPackageName() : null;
        onBestServiceChanged(packageName.equals(currentPackageName));
    }

    /**
     * Runs the given function asynchronously if and only if currently connected. Suppresses any
     * RemoteException thrown during execution.
     */
    public final void runOnBinder(BinderRunner runner) {
        mHandler.post(() -> {
            if (mBinder == null) {
                return;
            }

            try {
                runner.run(mBinder);
            } catch (RuntimeException | RemoteException e) {
                // binders may propagate some specific non-RemoteExceptions from the other side
                // through the binder as well - we cannot allow those to crash the system server
                Log.e(TAG, getLogPrefix() + " exception running on " + mServiceInfo, e);
            }
        });
    }

    /**
     * Runs the given function synchronously if currently connected, and returns the default value
     * if not currently connected or if any exception is thrown. Do not obtain any locks within the
     * BlockingBinderRunner, or risk deadlock. The default value will be returned if there is no
     * service connection when this is run, if a RemoteException occurs, or if the operation times
     * out.
     *
     * @deprecated Using this function is an indication that your AIDL API is broken. Calls from
     * system server to outside MUST be one-way, and so cannot return any result, and this
     * method should not be needed or used. Use a separate callback interface to allow outside
     * components to return results back to the system server.
     */
    @Deprecated
    public final <T> T runOnBinderBlocking(BlockingBinderRunner<T> runner, T defaultValue) {
        try {
            return runOnHandlerBlocking(() -> {
                if (mBinder == null) {
                    return defaultValue;
                }

                try {
                    return runner.run(mBinder);
                } catch (RuntimeException | RemoteException e) {
                    // binders may propagate some specific non-RemoteExceptions from the other side
                    // through the binder as well - we cannot allow those to crash the system server
                    Log.e(TAG, getLogPrefix() + " exception running on " + mServiceInfo, e);
                    return defaultValue;
                }
            });
        } catch (InterruptedException | TimeoutException e) {
            return defaultValue;
        }
    }

    private <T> T runOnHandlerBlocking(Callable<T> c)
            throws InterruptedException, TimeoutException {
        if (Looper.myLooper() == mHandler.getLooper()) {
            try {
                return c.call();
            } catch (Exception e) {
                // Function cannot throw exception, this should never happen
                throw new IllegalStateException(e);
            }
        } else {
            FutureTask<T> task = new FutureTask<>(c);
            mHandler.post(task);
            try {
                // timeout will unblock callers, in particular if the caller is a binder thread to
                // help reduce binder contention. this will still result in blocking the handler
                // thread which may result in ANRs, but should make problems slightly more rare.
                // the underlying solution is simply not to use this API at all, but that would
                // require large refactors to very legacy code.
                return task.get(BLOCKING_BINDER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            } catch (ExecutionException e) {
                // Function cannot throw exception, this should never happen
                throw new IllegalStateException(e);
            }
        }
    }

    private String getLogPrefix() {
        return "[" + mIntent.getAction() + "]";
    }

    @Override
    public String toString() {
        return mServiceInfo.toString();
    }

    /**
     * Dump for debugging.
     */
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("service=" + mServiceInfo);
        pw.println("connected=" + (mBinder != null));
    }
}
