/**
 * 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.vr;

import android.annotation.NonNull;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;

import com.android.internal.content.PackageMonitor;
import com.android.server.vr.SettingsObserver.SettingChangeListener;

import java.util.Collection;
import java.util.List;
import java.util.Set;

/**
 * Detects changes in packages, settings, and current users that may affect whether components
 * implementing a given service can be run.
 *
 * @hide
 */
public class EnabledComponentsObserver implements SettingChangeListener {

    private static final String TAG = EnabledComponentsObserver.class.getSimpleName();
    private static final String ENABLED_SERVICES_SEPARATOR = ":";

    public static final int NO_ERROR = 0;
    public static final int DISABLED = -1;
    public static final int NOT_INSTALLED = -2;

    private final Object mLock;
    private final Context mContext;
    private final String mSettingName;
    private final String mServiceName;
    private final String mServicePermission;
    private final SparseArray<ArraySet<ComponentName>> mInstalledSet = new SparseArray<>();
    private final SparseArray<ArraySet<ComponentName>> mEnabledSet = new SparseArray<>();
    private final Set<EnabledComponentChangeListener> mEnabledComponentListeners = new ArraySet<>();

    /**
     * Implement this to receive callbacks when relevant changes to the allowed components occur.
     */
    public interface EnabledComponentChangeListener {

        /**
         * Called when a change in the allowed components occurs.
         */
        void onEnabledComponentChanged();
    }

    private EnabledComponentsObserver(@NonNull Context context, @NonNull String settingName,
            @NonNull String servicePermission, @NonNull String serviceName, @NonNull Object lock,
            @NonNull Collection<EnabledComponentChangeListener> listeners) {
        mLock = lock;
        mContext = context;
        mSettingName = settingName;
        mServiceName = serviceName;
        mServicePermission = servicePermission;
        mEnabledComponentListeners.addAll(listeners);
    }

    /**
     * Create a EnabledComponentObserver instance.
     *
     * @param context the context to query for changes.
     * @param handler a handler to receive lifecycle events from system services on.
     * @param settingName the name of a setting to monitor for a list of enabled components.
     * @param looper a {@link Looper} to use for receiving package callbacks.
     * @param servicePermission the permission required by the components to be bound.
     * @param serviceName the intent action implemented by the tracked components.
     * @param lock a lock object used to guard instance state in all callbacks and method calls.
     * @return an EnableComponentObserver instance.
     */
    public static EnabledComponentsObserver build(@NonNull Context context,
            @NonNull Handler handler, @NonNull String settingName, @NonNull Looper looper,
            @NonNull String servicePermission, @NonNull String serviceName,
            @NonNull final Object lock,
            @NonNull Collection<EnabledComponentChangeListener> listeners) {

        SettingsObserver s = SettingsObserver.build(context, handler, settingName);

        final EnabledComponentsObserver o = new EnabledComponentsObserver(context, settingName,
                servicePermission, serviceName, lock, listeners);

        PackageMonitor packageMonitor = new PackageMonitor() {
            @Override
            public void onSomePackagesChanged() {
                o.onPackagesChanged();

            }

            @Override
            public void onPackageDisappeared(String packageName, int reason) {
                o.onPackagesChanged();

            }

            @Override
            public void onPackageModified(String packageName) {
                o.onPackagesChanged();

            }

            @Override
            public boolean onHandleForceStop(Intent intent, String[] packages, int uid,
                    boolean doit) {
                o.onPackagesChanged();

                return super.onHandleForceStop(intent, packages, uid, doit);
            }
        };

        packageMonitor.register(context, looper, UserHandle.ALL, true);

        s.addListener(o);

        return o;

    }

    public void onPackagesChanged() {
        rebuildAll();
    }

    @Override
    public void onSettingChanged() {
        rebuildAll();
    }

    @Override
    public void onSettingRestored(String prevValue, String newValue, int userId) {
        rebuildAll();
    }

    public void onUsersChanged() {
        rebuildAll();
    }

    /**
     * Rebuild the sets of allowed components for each current user profile.
     */
    public void rebuildAll() {
        synchronized (mLock) {
            mInstalledSet.clear();
            mEnabledSet.clear();
            final int[] userIds = getCurrentProfileIds();
            for (int i : userIds) {
                ArraySet<ComponentName> implementingPackages = loadComponentNamesForUser(i);
                ArraySet<ComponentName> packagesFromSettings =
                        loadComponentNamesFromSetting(mSettingName, i);
                packagesFromSettings.retainAll(implementingPackages);

                mInstalledSet.put(i, implementingPackages);
                mEnabledSet.put(i, packagesFromSettings);

            }
        }
        sendSettingChanged();
    }

    /**
     * Check whether a given component is present and enabled for the given user.
     *
     * @param component the component to check.
     * @param userId the user ID for the component to check.
     * @return {@code true} if present and enabled.
     */
    public int isValid(ComponentName component, int userId) {
        synchronized (mLock) {
            ArraySet<ComponentName> installedComponents = mInstalledSet.get(userId);
            if (installedComponents == null || !installedComponents.contains(component)) {
                return NOT_INSTALLED;
            }
            ArraySet<ComponentName> validComponents = mEnabledSet.get(userId);
            if (validComponents == null || !validComponents.contains(component)) {
                return DISABLED;
            }
            return NO_ERROR;
        }
    }

    /**
     * Return all VrListenerService components installed for this user.
     *
     * @param userId ID of the user to check.
     * @return a set of {@link ComponentName}s.
     */
    public ArraySet<ComponentName> getInstalled(int userId) {
        synchronized (mLock) {
            return mInstalledSet.get(userId);
        }
    }

    /**
     * Return all VrListenerService components enabled for this user.
     *
     * @param userId ID of the user to check.
     * @return a set of {@link ComponentName}s.
     */
    public ArraySet<ComponentName> getEnabled(int userId) {
        synchronized (mLock) {
            return mEnabledSet.get(userId);
        }
    }

    private int[] getCurrentProfileIds() {
        UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        if (userManager == null) {
            return null;
        }
        return userManager.getEnabledProfileIds(ActivityManager.getCurrentUser());
    }

    public static ArraySet<ComponentName> loadComponentNames(PackageManager pm, int userId,
            String serviceName, String permissionName) {

        ArraySet<ComponentName> installed = new ArraySet<>();
        Intent queryIntent = new Intent(serviceName);
        List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser(
                queryIntent,
                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
                userId);
        if (installedServices != null) {
            for (int i = 0, count = installedServices.size(); i < count; i++) {
                ResolveInfo resolveInfo = installedServices.get(i);
                ServiceInfo info = resolveInfo.serviceInfo;

                ComponentName component = new ComponentName(info.packageName, info.name);
                if (!permissionName.equals(info.permission)) {
                    Slog.w(TAG, "Skipping service " + info.packageName + "/" + info.name
                            + ": it does not require the permission "
                            + permissionName);
                    continue;
                }
                installed.add(component);
            }
        }
        return installed;
    }

    private ArraySet<ComponentName> loadComponentNamesForUser(int userId) {
        return loadComponentNames(mContext.getPackageManager(), userId, mServiceName,
                mServicePermission);
    }

    private ArraySet<ComponentName> loadComponentNamesFromSetting(String settingName,
            int userId) {
        final ContentResolver cr = mContext.getContentResolver();
        String settingValue = Settings.Secure.getStringForUser(
                cr,
                settingName,
                userId);
        if (TextUtils.isEmpty(settingValue))
            return new ArraySet<>();
        String[] restored = settingValue.split(ENABLED_SERVICES_SEPARATOR);
        ArraySet<ComponentName> result = new ArraySet<>(restored.length);
        for (int i = 0; i < restored.length; i++) {
            ComponentName value = ComponentName.unflattenFromString(restored[i]);
            if (null != value) {
                result.add(value);
            }
        }
        return result;
    }

    private void sendSettingChanged() {
        for (EnabledComponentChangeListener l : mEnabledComponentListeners) {
            l.onEnabledComponentChanged();
        }
    }

}
