/*
 * Copyright (C) 2023 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.pm;

import static android.content.pm.PackageManager.PROPERTY_LEGACY_UPDATE_OWNERSHIP_DENYLIST;

import static com.android.server.pm.PackageManagerService.TAG;

import android.Manifest;
import android.app.ResourcesManager;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.pm.pkg.component.ParsedUsesPermission;
import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
import com.android.server.pm.pkg.AndroidPackage;

import org.xmlpull.v1.XmlPullParser;

import java.util.Collection;

/** Helper class for managing update ownership and optouts for the feature. */
public class UpdateOwnershipHelper {

    // Called out in PackageManager.PROPERTY_LEGACY_UPDATE_OWNERSHIP_DENYLIST docs
    private static final int MAX_DENYLIST_SIZE = 500;
    private static final String TAG_OWNERSHIP_OPT_OUT = "deny-ownership";
    private final ArrayMap<String, ArraySet<String>> mUpdateOwnerOptOutsToOwners =
            new ArrayMap<>(200);

    private final Object mLock = new Object();

    static boolean hasValidOwnershipDenyList(PackageSetting pkgSetting) {
        AndroidPackage pkg = pkgSetting.getPkg();
        // we're checking for uses-permission for these priv permissions instead of grant as we're
        // only considering system apps to begin with, so presumed to be granted.
        return pkg != null
                && (pkgSetting.isSystem() || pkgSetting.isUpdatedSystemApp())
                && pkg.getProperties().containsKey(PROPERTY_LEGACY_UPDATE_OWNERSHIP_DENYLIST)
                && usesAnyPermission(pkg,
                        Manifest.permission.INSTALL_PACKAGES,
                        Manifest.permission.INSTALL_PACKAGE_UPDATES);
    }


    /** Returns true if a package setting declares that it uses a permission */
    private static boolean usesAnyPermission(AndroidPackage pkgSetting, String... permissions) {
        Collection<ParsedUsesPermission> usesPermissions =
                pkgSetting.getUsesPermissionMapping().values();
        for (ParsedUsesPermission usesPermission : usesPermissions) {
            for (int j = 0; j < permissions.length; j++) {
                if (permissions[j].equals(usesPermission.getName())) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Reads the update owner deny list from a {@link PackageSetting} and returns the set of
     * packages it contains or {@code null} if it cannot be read.
     */
    public ArraySet<String> readUpdateOwnerDenyList(PackageSetting pkgSetting) {
        if (!hasValidOwnershipDenyList(pkgSetting)) {
            return null;
        }
        AndroidPackage pkg = pkgSetting.getPkg();
        if (pkg == null) {
            return null;
        }
        ArraySet<String> ownershipDenyList = new ArraySet<>(MAX_DENYLIST_SIZE);
        try {
            int resId = pkg.getProperties().get(PROPERTY_LEGACY_UPDATE_OWNERSHIP_DENYLIST)
                    .getResourceId();
            ApplicationInfo appInfo = AndroidPackageUtils.generateAppInfoWithoutState(pkg);
            Resources resources = ResourcesManager.getInstance().getResources(
                    null, appInfo.sourceDir, appInfo.splitSourceDirs, appInfo.resourceDirs,
                    appInfo.overlayPaths, appInfo.sharedLibraryFiles, null, Configuration.EMPTY,
                    null, null, null);
            try (XmlResourceParser parser = resources.getXml(resId)) {
                while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
                    if (parser.next() == XmlResourceParser.START_TAG) {
                        if (TAG_OWNERSHIP_OPT_OUT.equals(parser.getName())) {
                            parser.next();
                            String packageName = parser.getText();
                            if (packageName != null && !packageName.isBlank()) {
                                ownershipDenyList.add(packageName);
                                if (ownershipDenyList.size() > MAX_DENYLIST_SIZE) {
                                    Slog.w(TAG, "Deny list defined by " + pkg.getPackageName()
                                            + " was trucated to maximum size of "
                                            + MAX_DENYLIST_SIZE);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            Slog.e(TAG, "Failed to parse update owner list for " + pkgSetting.getPackageName(), e);
            return null;
        }
        return ownershipDenyList;
    }

    /**
     * Begins tracking the contents of a deny list and the owner of that deny list for use in calls
     * to {@link #isUpdateOwnershipDenylisted(String)} and
     * {@link #isUpdateOwnershipDenyListProvider(String)}.
     *
     * @param listOwner the packageName of the package that owns the deny list.
     * @param listContents the list of packageNames that are on the deny list.
     */
    public void addToUpdateOwnerDenyList(String listOwner, ArraySet<String> listContents) {
        synchronized (mLock) {
            for (int i = 0; i < listContents.size(); i++) {
                String packageName = listContents.valueAt(i);
                ArraySet<String> priorDenyListOwners = mUpdateOwnerOptOutsToOwners.putIfAbsent(
                        packageName, new ArraySet<>(new String[]{listOwner}));
                if (priorDenyListOwners != null) {
                    priorDenyListOwners.add(listOwner);
                }
            }
        }
    }

    /**
     * Stop tracking the contents of a deny list owned by the provided owner of the deny list.
     * @param listOwner the packageName of the package that owns the deny list.
     */
    public void removeUpdateOwnerDenyList(String listOwner) {
        synchronized (mLock) {
            for (int i = mUpdateOwnerOptOutsToOwners.size() - 1; i >= 0; i--) {
                ArraySet<String> packageDenyListContributors =
                        mUpdateOwnerOptOutsToOwners.get(mUpdateOwnerOptOutsToOwners.keyAt(i));
                if (packageDenyListContributors.remove(listOwner)
                        && packageDenyListContributors.isEmpty()) {
                    mUpdateOwnerOptOutsToOwners.removeAt(i);
                }
            }
        }
    }

    /**
     * Returns {@code true} if the provided package name is on a valid update ownership deny list.
     */
    public boolean isUpdateOwnershipDenylisted(String packageName) {
        return mUpdateOwnerOptOutsToOwners.containsKey(packageName);
    }

    /**
     * Returns {@code true} if the provided package name defines a valid update ownership deny list.
     */
    public boolean isUpdateOwnershipDenyListProvider(String packageName) {
        if (packageName == null) {
            return false;
        }
        synchronized (mLock) {
            for (int i = mUpdateOwnerOptOutsToOwners.size() - 1; i >= 0; i--) {
                if (mUpdateOwnerOptOutsToOwners.valueAt(i).contains(packageName)) {
                    return true;
                }
            }
            return false;
        }
    }
}
