/*
 * 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 static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Context.BIND_NOT_FOREGROUND;
import static android.content.Context.BIND_NOT_VISIBLE;

import android.annotation.Nullable;
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.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.PackageMonitor;
import com.android.internal.util.Preconditions;
import com.android.server.servicewatcher.ServiceWatcher.BoundServiceInfo;
import com.android.server.servicewatcher.ServiceWatcher.ServiceChangedListener;

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

/**
 * Implementation of ServiceWatcher. Keeping the implementation separate from the interface allows
 * us to store the generic relationship between the service supplier and the service listener, while
 * hiding the generics from clients, simplifying the API.
 */
class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements ServiceWatcher,
        ServiceChangedListener {

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

    static final long RETRY_DELAY_MS = 15 * 1000;

    final Context mContext;
    final Handler mHandler;
    final String mTag;
    final ServiceSupplier<TBoundServiceInfo> mServiceSupplier;
    final @Nullable ServiceListener<? super TBoundServiceInfo> mServiceListener;

    private final PackageMonitor mPackageMonitor = new PackageMonitor() {
        @Override
        public boolean onPackageChanged(String packageName, int uid, String[] components) {
            return true;
        }

        @Override
        public void onSomePackagesChanged() {
            onServiceChanged(false);
        }
    };

    @GuardedBy("this")
    private boolean mRegistered = false;
    @GuardedBy("this")
    private MyServiceConnection mServiceConnection = new MyServiceConnection(null);

    ServiceWatcherImpl(Context context, Handler handler, String tag,
            ServiceSupplier<TBoundServiceInfo> serviceSupplier,
            ServiceListener<? super TBoundServiceInfo> serviceListener) {
        mContext = context;
        mHandler = handler;
        mTag = tag;
        mServiceSupplier = serviceSupplier;
        mServiceListener = serviceListener;
    }

    @Override
    public boolean checkServiceResolves() {
        return mServiceSupplier.hasMatchingService();
    }

    @Override
    public synchronized void register() {
        Preconditions.checkState(!mRegistered);

        mRegistered = true;
        mPackageMonitor.register(mContext, UserHandle.ALL, /*externalStorage=*/ true, mHandler);
        mServiceSupplier.register(this);

        onServiceChanged(false);
    }

    @Override
    public synchronized void unregister() {
        Preconditions.checkState(mRegistered);

        mServiceSupplier.unregister();
        mPackageMonitor.unregister();
        mRegistered = false;

        onServiceChanged(false);
    }

    @Override
    public synchronized void onServiceChanged() {
        onServiceChanged(false);
    }

    @Override
    public synchronized void runOnBinder(BinderOperation operation) {
        MyServiceConnection serviceConnection = mServiceConnection;
        mHandler.post(() -> serviceConnection.runOnBinder(operation));
    }

    synchronized void onServiceChanged(boolean forceRebind) {
        TBoundServiceInfo newBoundServiceInfo;
        if (mRegistered) {
            newBoundServiceInfo = mServiceSupplier.getServiceInfo();
        } else {
            newBoundServiceInfo = null;
        }

        if (forceRebind || !Objects.equals(mServiceConnection.getBoundServiceInfo(),
                newBoundServiceInfo)) {
            Log.i(TAG, "[" + mTag + "] chose new implementation " + newBoundServiceInfo);
            MyServiceConnection oldServiceConnection = mServiceConnection;
            MyServiceConnection newServiceConnection = new MyServiceConnection(newBoundServiceInfo);
            mServiceConnection = newServiceConnection;
            mHandler.post(() -> {
                oldServiceConnection.unbind();
                newServiceConnection.bind();
            });
        }
    }

    @Override
    public String toString() {
        MyServiceConnection serviceConnection;
        synchronized (this) {
            serviceConnection = mServiceConnection;
        }

        return serviceConnection.getBoundServiceInfo().toString();
    }

    @Override
    public void dump(PrintWriter pw) {
        MyServiceConnection serviceConnection;
        synchronized (this) {
            serviceConnection = mServiceConnection;
        }

        pw.println("target service=" + serviceConnection.getBoundServiceInfo());
        pw.println("connected=" + serviceConnection.isConnected());
    }

    // runs on the handler thread, and expects most of it's methods to be called from that thread
    private class MyServiceConnection implements ServiceConnection {

        private final @Nullable TBoundServiceInfo mBoundServiceInfo;

        // volatile so that isConnected can be called from any thread easily
        private volatile @Nullable IBinder mBinder;
        private @Nullable Runnable mRebinder;

        MyServiceConnection(@Nullable TBoundServiceInfo boundServiceInfo) {
            mBoundServiceInfo = boundServiceInfo;
        }

        // may be called from any thread
        @Nullable TBoundServiceInfo getBoundServiceInfo() {
            return mBoundServiceInfo;
        }

        // may be called from any thread
        boolean isConnected() {
            return mBinder != null;
        }

        void bind() {
            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());

            if (mBoundServiceInfo == null) {
                return;
            }

            if (D) {
                Log.d(TAG, "[" + mTag + "] binding to " + mBoundServiceInfo);
            }

            Intent bindIntent = new Intent(mBoundServiceInfo.getAction()).setComponent(
                    mBoundServiceInfo.getComponentName());
            if (!mContext.bindServiceAsUser(bindIntent, this,
                    BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE,
                    mHandler, UserHandle.of(mBoundServiceInfo.getUserId()))) {
                Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later");
                mRebinder = this::bind;
                mHandler.postDelayed(mRebinder, RETRY_DELAY_MS);
            } else {
                mRebinder = null;
            }
        }

        void unbind() {
            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());

            if (mBoundServiceInfo == null) {
                return;
            }

            if (D) {
                Log.d(TAG, "[" + mTag + "] unbinding from " + mBoundServiceInfo);
            }

            if (mRebinder != null) {
                mHandler.removeCallbacks(mRebinder);
                mRebinder = null;
            } else {
                mContext.unbindService(this);
            }

            onServiceDisconnected(mBoundServiceInfo.getComponentName());
        }

        void runOnBinder(BinderOperation operation) {
            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());

            if (mBinder == null) {
                operation.onError();
                return;
            }

            try {
                operation.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, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e);
                operation.onError();
            }
        }

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

            Log.i(TAG, "[" + mTag + "] connected to " + component.toShortString());

            mBinder = binder;

            if (mServiceListener != null) {
                try {
                    mServiceListener.onBind(binder, mBoundServiceInfo);
                } 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, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e);
                }
            }
        }

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

            if (mBinder == null) {
                return;
            }

            Log.i(TAG, "[" + mTag + "] disconnected from " + mBoundServiceInfo);

            mBinder = null;
            if (mServiceListener != null) {
                mServiceListener.onUnbind();
            }
        }

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

            Log.w(TAG, "[" + mTag + "] " + mBoundServiceInfo + " died");

            // introduce a small delay to prevent spamming binding over and over, since the likely
            // cause of a binding dying is some package event that may take time to recover from
            mHandler.postDelayed(() -> onServiceChanged(true), 500);
        }

        @Override
        public final void onNullBinding(ComponentName component) {
            Log.e(TAG, "[" + mTag + "] " + mBoundServiceInfo + " has null binding");
        }
    }
}
