/*
 * 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.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 static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.UserHandle.USER_SYSTEM;

import android.annotation.BoolRes;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.app.ActivityManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.location.flags.Flags;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.util.Log;
import android.util.TypedValue;

import com.android.internal.util.Preconditions;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.servicewatcher.ServiceWatcher.ServiceChangedListener;
import com.android.server.servicewatcher.ServiceWatcher.ServiceSupplier;

import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * Supplies services based on the current active user and version as defined in the service
 * manifest. This implementation uses {@link android.content.pm.PackageManager#MATCH_SYSTEM_ONLY} to
 * ensure only system (ie, privileged) services are matched. It also handles services that are not
 * direct boot aware, and will automatically pick the best service as the user's direct boot state
 * changes.
 *
 * <p>Optionally, two permissions may be specified: (1) a caller permission - any service that does
 * not require callers to hold this permission is rejected (2) a service permission - any service
 * whose package does not hold this permission is rejected.
 *
 * @hide
 */
public final class CurrentUserServiceSupplier extends BroadcastReceiver implements
        ServiceSupplier<CurrentUserServiceSupplier.BoundServiceInfo> {

    private static final String TAG = "CurrentUserServiceSupplier";

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

    // a package value that will never match against any package (we can't use null since this will
    // match against any package).
    private static final String NO_MATCH_PACKAGE = "";

    private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> {
        if (o1 == o2) {
            return 0;
        } else if (o1 == null) {
            return -1;
        } else if (o2 == null) {
            return 1;
        }

        // ServiceInfos with higher version numbers always win. if version numbers are equal
        // 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(o1.getVersion(), o2.getVersion());
        if (ret == 0) {
            if (o1.getUserId() != USER_SYSTEM && o2.getUserId() == USER_SYSTEM) {
                ret = -1;
            } else if (o1.getUserId() == USER_SYSTEM && o2.getUserId() != USER_SYSTEM) {
                ret = 1;
            }
        }
        return ret;
    };

    /** Bound service information with version information. */
    public static class BoundServiceInfo extends ServiceWatcher.BoundServiceInfo {

        private static int parseUid(ResolveInfo resolveInfo) {
            int uid = resolveInfo.serviceInfo.applicationInfo.uid;
            Bundle metadata = resolveInfo.serviceInfo.metaData;
            if (metadata != null && metadata.getBoolean(EXTRA_SERVICE_IS_MULTIUSER, false)) {
                // reconstruct a uid for the same app but with the system user - hope this exists
                uid = UserHandle.getUid(USER_SYSTEM, UserHandle.getAppId(uid));
            }
            return uid;
        }

        private static int parseVersion(ResolveInfo resolveInfo) {
            int version = Integer.MIN_VALUE;
            if (resolveInfo.serviceInfo.metaData != null) {
                version = resolveInfo.serviceInfo.metaData.getInt(EXTRA_SERVICE_VERSION, version);
            }
            return version;
        }

        private final int mVersion;
        private final @Nullable Bundle mMetadata;

        protected BoundServiceInfo(String action, ResolveInfo resolveInfo) {
            this(action, parseUid(resolveInfo), resolveInfo.serviceInfo.getComponentName(),
                    parseVersion(resolveInfo), resolveInfo.serviceInfo.metaData);
        }

        protected BoundServiceInfo(String action, int uid, ComponentName componentName, int version,
                @Nullable Bundle metadata) {
            super(action, uid, componentName);

            mVersion = version;
            mMetadata = metadata;
        }

        public int getVersion() {
            return mVersion;
        }

        public @Nullable Bundle getMetadata() {
            return mMetadata;
        }

        @Override
        public String toString() {
            return super.toString() + "@" + mVersion;
        }
    }

    /**
     * Creates an instance using package details retrieved from config.
     *
     * @see #create(Context, String, String, String, String)
     */
    public static CurrentUserServiceSupplier createFromConfig(Context context, String action,
            @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) {
        String explicitPackage = retrieveExplicitPackage(context, enableOverlayResId,
                nonOverlayPackageResId);
        return CurrentUserServiceSupplier.create(context, action, explicitPackage,
                /*callerPermission=*/null, /*servicePermission=*/null);
    }

    /**
     * Creates an instance with the specific service details and permission requirements.
     *
     * @param context the context the supplier is to use
     * @param action the action the service must declare in its intent-filter
     * @param explicitPackage the package of the service, or {@code null} if the package of the
     *     service is not constrained
     * @param callerPermission a permission that the service forces callers (i.e.
     *     ServiceWatcher/system server) to hold, or {@code null} if there isn't one
     * @param servicePermission a permission that the service package should hold, or {@code null}
     *     if there isn't one
     */
    public static CurrentUserServiceSupplier create(Context context, String action,
            @Nullable String explicitPackage, @Nullable String callerPermission,
            @Nullable String servicePermission) {
        boolean matchSystemAppsOnly = true;
        return new CurrentUserServiceSupplier(context, action,
                explicitPackage, callerPermission, servicePermission, matchSystemAppsOnly);
    }

    /**
     * Creates an instance like {@link #create} except it allows connection to services that are not
     * supplied by system packages. Only intended for use during tests.
     *
     * @see #create(Context, String, String, String, String)
     */
    public static CurrentUserServiceSupplier createUnsafeForTestsOnly(Context context,
            String action, @Nullable String explicitPackage, @Nullable String callerPermission,
            @Nullable String servicePermission) {
        boolean matchSystemAppsOnly = false;
        return new CurrentUserServiceSupplier(context, action,
                explicitPackage, callerPermission, servicePermission, matchSystemAppsOnly);
    }

    private static @Nullable String retrieveExplicitPackage(Context context,
            @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) {
        Resources resources = context.getResources();
        boolean enableOverlay = resources.getBoolean(enableOverlayResId);
        if (!enableOverlay) {
            if (Flags.fixServiceWatcher()) {
                // we don't use getText() or similar because it won't return null values
                TypedValue out = new TypedValue();
                resources.getValue(nonOverlayPackageResId, out, true);
                CharSequence explicitPackage = out.coerceToString();
                if (explicitPackage == null) {
                    return NO_MATCH_PACKAGE;
                } else {
                    return explicitPackage.toString();
                }
            } else {
                return resources.getString(nonOverlayPackageResId);
            }
        } else {
            return null;
        }
    }

    private final Context mContext;
    private final ActivityManagerInternal mActivityManager;
    private final Intent mIntent;
    // a permission that the service forces callers (ie ServiceWatcher/system server) to hold
    private final @Nullable String mCallerPermission;
    // a permission that the service package should hold
    private final @Nullable String mServicePermission;
    // whether to use MATCH_SYSTEM_ONLY in queries
    private final boolean mMatchSystemAppsOnly;

    private volatile ServiceChangedListener mListener;
    private @Nullable String mUnstableService;

    private CurrentUserServiceSupplier(Context context, String action,
            @Nullable String explicitPackage, @Nullable String callerPermission,
            @Nullable String servicePermission, boolean matchSystemAppsOnly) {
        mContext = context;
        mActivityManager = Objects.requireNonNull(
                LocalServices.getService(ActivityManagerInternal.class));
        mIntent = new Intent(action);

        if (explicitPackage != null) {
            mIntent.setPackage(explicitPackage);
        }

        mCallerPermission = callerPermission;
        mServicePermission = servicePermission;
        mMatchSystemAppsOnly = matchSystemAppsOnly;
    }

    @Override
    public boolean hasMatchingService() {
        if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) {
            return false;
        }

        int intentQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
        if (mMatchSystemAppsOnly) {
            intentQueryFlags |= MATCH_SYSTEM_ONLY;
        }
        List<ResolveInfo> resolveInfos = mContext.getPackageManager()
                .queryIntentServicesAsUser(mIntent,
                        intentQueryFlags,
                        UserHandle.USER_SYSTEM);
        return !resolveInfos.isEmpty();
    }

    @Override
    public void register(ServiceChangedListener listener) {
        Preconditions.checkState(mListener == null);

        mListener = listener;

        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
        intentFilter.addAction(Intent.ACTION_USER_UNLOCKED);
        intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
        mContext.registerReceiverAsUser(this, UserHandle.ALL, intentFilter, null,
                FgThread.getHandler());
    }

    @Override
    public void unregister() {
        Preconditions.checkArgument(mListener != null);

        mListener = null;
        mContext.unregisterReceiver(this);
    }

    @Override
    public BoundServiceInfo getServiceInfo() {
        if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) {
            return null;
        }

        BoundServiceInfo bestServiceInfo = null;

        // only allow services in the correct direct boot state to match
        int intentQueryFlags = MATCH_DIRECT_BOOT_AUTO | GET_META_DATA;
        if (mMatchSystemAppsOnly) {
            intentQueryFlags |= MATCH_SYSTEM_ONLY;
        }
        int currentUserId = mActivityManager.getCurrentUserId();
        List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
                mIntent,
                intentQueryFlags,
                currentUserId);
        for (ResolveInfo resolveInfo : resolveInfos) {
            ServiceInfo service = Objects.requireNonNull(resolveInfo.serviceInfo);

            if (mCallerPermission != null) {
                if (!mCallerPermission.equals(service.permission)) {
                    Log.d(TAG, service.getComponentName().flattenToShortString()
                            + " disqualified due to not requiring " + mCallerPermission);
                    continue;
                }
            }

            BoundServiceInfo serviceInfo = new BoundServiceInfo(mIntent.getAction(), resolveInfo);

            if (mServicePermission != null) {
                if (mContext.checkPermission(mServicePermission, Process.INVALID_PID,
                        serviceInfo.mUid) != PERMISSION_GRANTED) {
                    Log.d(TAG, serviceInfo.getComponentName().flattenToShortString()
                            + " disqualified due to not holding " + mCallerPermission);
                    continue;
                }
            }

            // Prefer any service over the unstable service.
            if (mUnstableService != null && serviceInfo != null && bestServiceInfo != null) {
              if (mUnstableService.equals(serviceInfo.toString())) {
                  Log.d(TAG, "Not choosing unstable service " + mUnstableService
                          + " as we already have a service " + bestServiceInfo.toString());
                  continue;
              } else if (mUnstableService.equals(bestServiceInfo.toString())) {
                  Log.d(TAG, "Choosing service " + serviceInfo.toString()
                          + " over the unstable service " + mUnstableService);
                  bestServiceInfo = serviceInfo;
                  continue;
              }
            }

            if (sBoundServiceInfoComparator.compare(serviceInfo, bestServiceInfo) > 0) {
                bestServiceInfo = serviceInfo;
            }
        }

        return bestServiceInfo;
    }

    /**
     * Alerts the supplier that the given service is unstable.
     *
     * The service marked as unstable will be unpreferred over any other services,
     * which will last until the next device restart.
     */
    @Override
    public void alertUnstableService(String unstableService) {
        mUnstableService = unstableService;
    }

    @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;
        }
        ServiceChangedListener listener = mListener;
        if (listener == null) {
            return;
        }

        switch (action) {
            case Intent.ACTION_USER_SWITCHED:
                listener.onServiceChanged();
                break;
            case Intent.ACTION_USER_UNLOCKED:
                // user unlocked implies direct boot mode may have changed
                if (userId == mActivityManager.getCurrentUserId()) {
                    listener.onServiceChanged();
                }
                break;
            default:
                break;
        }
    }
}
