/*
 * 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.settings.applications.appinfo;

import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_DEFAULT;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED;
import static android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM;
import static android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_UNKNOWN;
import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;

import static com.android.settings.Utils.PROPERTY_APP_HIBERNATION_ENABLED;
import static com.android.settings.Utils.PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS;

import android.app.AppOpsManager;
import android.apphibernation.AppHibernationManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.permission.PermissionControllerManager;
import android.provider.DeviceConfig;
import android.util.Slog;

import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.SwitchPreference;

import com.google.common.annotations.VisibleForTesting;

/**
 * A PreferenceController handling the logic for exempting hibernation of app
 */
public final class HibernationSwitchPreferenceController extends AppInfoPreferenceControllerBase
        implements Preference.OnPreferenceChangeListener {
    private static final String TAG = "HibernationSwitchPrefController";
    private String mPackageName;
    private final AppOpsManager mAppOpsManager;
    private final PermissionControllerManager mPermissionControllerManager;
    private int mPackageUid;
    private boolean mHibernationEligibilityLoaded;
    private int mHibernationEligibility = HIBERNATION_ELIGIBILITY_UNKNOWN;
    @VisibleForTesting
    boolean mIsPackageSet;
    private boolean mIsPackageExemptByDefault;

    public HibernationSwitchPreferenceController(Context context,
            String preferenceKey) {
        super(context, preferenceKey);
        mAppOpsManager = context.getSystemService(AppOpsManager.class);
        mPermissionControllerManager = context.getSystemService(PermissionControllerManager.class);
    }

    @Override
    public int getAvailabilityStatus() {
        return isHibernationEnabled() && mIsPackageSet ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
    }

    /**
     * Set the package. And also retrieve details from package manager. Some packages may be
     * exempted from hibernation by default. This method should only be called to initialize the
     * controller.
     * @param packageName The name of the package whose hibernation state to be managed.
     */
    void setPackage(@NonNull String packageName) {
        mPackageName = packageName;
        final PackageManager packageManager = mContext.getPackageManager();

        // Q- packages exempt by default, except R- on Auto since Auto-Revoke was skipped in R
        final int maxTargetSdkVersionForExemptApps =
                packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
                        ? android.os.Build.VERSION_CODES.R
                        : android.os.Build.VERSION_CODES.Q;
        try {
            mPackageUid = packageManager.getPackageUid(packageName, /* flags */ 0);
            mIsPackageExemptByDefault =
                    hibernationTargetsPreSApps()
                            ? false
                            : packageManager.getTargetSdkVersion(packageName)
                                    <= maxTargetSdkVersionForExemptApps;
            mIsPackageSet = true;
        } catch (PackageManager.NameNotFoundException e) {
            Slog.w(TAG, "Package [" + mPackageName + "] is not found!");
            mIsPackageSet = false;
        }
    }

    private boolean isAppEligibleForHibernation() {
        return mHibernationEligibilityLoaded
                && mHibernationEligibility != HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM
                && mHibernationEligibility != HIBERNATION_ELIGIBILITY_UNKNOWN;
    }

    @Override
    public void updateState(Preference preference) {
        super.updateState(preference);
        ((SwitchPreference) preference).setChecked(isAppEligibleForHibernation()
                && !isPackageHibernationExemptByUser());
        preference.setEnabled(isAppEligibleForHibernation());
        if (!mHibernationEligibilityLoaded) {
            mPermissionControllerManager.getHibernationEligibility(mPackageName,
                    mContext.getMainExecutor(),
                    eligibility -> {
                        mHibernationEligibility = eligibility;
                        mHibernationEligibilityLoaded = true;
                        updateState(preference);
                    });
        }
    }

    @VisibleForTesting
    boolean isPackageHibernationExemptByUser() {
        if (!mIsPackageSet) return true;
        final int mode = mAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid, mPackageName);

        return mode == MODE_DEFAULT ? mIsPackageExemptByDefault : mode != MODE_ALLOWED;
    }

    @Override
    public boolean onPreferenceChange(Preference preference, Object isChecked) {
        try {
            final boolean checked = (boolean) isChecked;
            mAppOpsManager.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid,
                    checked ? MODE_ALLOWED : MODE_IGNORED);
            if (!checked) {
                final AppHibernationManager ahm =
                        mContext.getSystemService(AppHibernationManager.class);
                ahm.setHibernatingForUser(mPackageName, false);
                ahm.setHibernatingGlobally(mPackageName, false);
            }
        } catch (RuntimeException e) {
            return false;
        }
        return true;
    }

    private static boolean isHibernationEnabled() {
        return DeviceConfig.getBoolean(
                NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, true);
    }

    private static boolean hibernationTargetsPreSApps() {
        return DeviceConfig.getBoolean(
                NAMESPACE_APP_HIBERNATION, PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS, false);
    }
}
