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

import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.backup.BackupDataInputStream;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupHelper;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.os.Build;
import android.os.IDeviceIdleController;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;

import androidx.annotation.VisibleForTesting;

import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;

import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** An implementation to backup and restore battery configurations. */
public final class BatteryBackupHelper implements BackupHelper {
    /** An inditifier for {@link BackupHelper}. */
    public static final String TAG = "BatteryBackupHelper";
    // Definition for the device build information.
    public static final String KEY_BUILD_BRAND = "device_build_brand";
    public static final String KEY_BUILD_PRODUCT = "device_build_product";
    public static final String KEY_BUILD_MANUFACTURER = "device_build_manufacture";
    public static final String KEY_BUILD_FINGERPRINT = "device_build_fingerprint";
    // Customized fields for device extra information.
    public static final String KEY_BUILD_METADATA_1 = "device_build_metadata_1";
    public static final String KEY_BUILD_METADATA_2 = "device_build_metadata_2";

    private static final String DEVICE_IDLE_SERVICE = "deviceidle";
    private static final String BATTERY_OPTIMIZE_BACKUP_FILE_NAME =
            "battery_optimize_backup_historical_logs";
    private static final int DEVICE_BUILD_INFO_SIZE = 6;

    static final String DELIMITER = ",";
    static final String DELIMITER_MODE = ":";
    static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list";

    @VisibleForTesting
    ArraySet<ApplicationInfo> mTestApplicationInfoList = null;

    @VisibleForTesting
    PowerAllowlistBackend mPowerAllowlistBackend;
    @VisibleForTesting
    IDeviceIdleController mIDeviceIdleController;
    @VisibleForTesting
    IPackageManager mIPackageManager;
    @VisibleForTesting
    BatteryOptimizeUtils mBatteryOptimizeUtils;

    private byte[] mOptimizationModeBytes;
    private boolean mVerifyMigrateConfiguration = false;

    private final Context mContext;
    // Device information map from the restoreEntity() method.
    private final ArrayMap<String, String> mDeviceBuildInfoMap =
            new ArrayMap<>(DEVICE_BUILD_INFO_SIZE);

    public BatteryBackupHelper(Context context) {
        mContext = context.getApplicationContext();
    }

    @Override
    public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
            ParcelFileDescriptor newState) {
        if (!isOwner() || data == null) {
            Log.w(TAG, "ignore performBackup() for non-owner or empty data");
            return;
        }
        final List<String> allowlistedApps = getFullPowerList();
        if (allowlistedApps == null) {
            return;
        }

        writeBackupData(data, KEY_BUILD_BRAND, Build.BRAND);
        writeBackupData(data, KEY_BUILD_PRODUCT, Build.PRODUCT);
        writeBackupData(data, KEY_BUILD_MANUFACTURER, Build.MANUFACTURER);
        writeBackupData(data, KEY_BUILD_FINGERPRINT, Build.FINGERPRINT);
        // Add customized device build metadata fields.
        final PowerUsageFeatureProvider provider = FeatureFactory.getFactory(mContext)
                .getPowerUsageFeatureProvider(mContext);
        writeBackupData(data, KEY_BUILD_METADATA_1, provider.getBuildMetadata1(mContext));
        writeBackupData(data, KEY_BUILD_METADATA_2, provider.getBuildMetadata2(mContext));

        backupOptimizationMode(data, allowlistedApps);
    }

    @Override
    public void restoreEntity(BackupDataInputStream data) {
        // Ensure we only verify the migrate configuration one time.
        if (!mVerifyMigrateConfiguration) {
            mVerifyMigrateConfiguration = true;
            BatterySettingsMigrateChecker.verifySaverConfiguration(mContext);
        }
        if (!isOwner() || data == null || data.size() == 0) {
            Log.w(TAG, "ignore restoreEntity() for non-owner or empty data");
            return;
        }
        final String dataKey = data.getKey();
        switch (dataKey) {
            case KEY_BUILD_BRAND:
            case KEY_BUILD_PRODUCT:
            case KEY_BUILD_MANUFACTURER:
            case KEY_BUILD_FINGERPRINT:
            case KEY_BUILD_METADATA_1:
            case KEY_BUILD_METADATA_2:
                restoreBackupData(dataKey, data);
                break;
            case KEY_OPTIMIZATION_LIST:
                // Hold the optimization mode data until all conditions are matched.
                mOptimizationModeBytes = getBackupData(dataKey, data);
                break;
        }
        performRestoreIfNeeded();
    }

    @Override
    public void writeNewStateDescription(ParcelFileDescriptor newState) {
    }

    private List<String> getFullPowerList() {
        final long timestamp = System.currentTimeMillis();
        String[] allowlistedApps;
        try {
            allowlistedApps = getIDeviceIdleController().getFullPowerWhitelist();
        } catch (RemoteException e) {
            Log.e(TAG, "backupFullPowerList() failed", e);
            return null;
        }
        // Ignores unexpected empty result case.
        if (allowlistedApps == null || allowlistedApps.length == 0) {
            Log.w(TAG, "no data found in the getFullPowerList()");
            return new ArrayList<>();
        }
        Log.d(TAG, String.format("getFullPowerList() size=%d in %d/ms",
                allowlistedApps.length, (System.currentTimeMillis() - timestamp)));
        return Arrays.asList(allowlistedApps);
    }

    @VisibleForTesting
    void backupOptimizationMode(BackupDataOutput data, List<String> allowlistedApps) {
        final long timestamp = System.currentTimeMillis();
        final ArraySet<ApplicationInfo> applications = getInstalledApplications();
        if (applications == null || applications.isEmpty()) {
            Log.w(TAG, "no data found in the getInstalledApplications()");
            return;
        }
        int backupCount = 0;
        final StringBuilder builder = new StringBuilder();
        final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
        final SharedPreferences sharedPreferences = getSharedPreferences(mContext);
        // Converts application into the AppUsageState.
        for (ApplicationInfo info : applications) {
            final int mode = BatteryOptimizeUtils.getMode(appOps, info.uid, info.packageName);
            @BatteryOptimizeUtils.OptimizationMode
            final int optimizationMode = BatteryOptimizeUtils.getAppOptimizationMode(
                    mode, allowlistedApps.contains(info.packageName));
            // Ignores default optimized/unknown state or system/default apps.
            if (optimizationMode == BatteryOptimizeUtils.MODE_OPTIMIZED
                    || optimizationMode == BatteryOptimizeUtils.MODE_UNKNOWN
                    || isSystemOrDefaultApp(info.packageName, info.uid)) {
                continue;
            }
            final String packageOptimizeMode =
                    info.packageName + DELIMITER_MODE + optimizationMode;
            builder.append(packageOptimizeMode + DELIMITER);
            Log.d(TAG, "backupOptimizationMode: " + packageOptimizeMode);
            BatteryOptimizeLogUtils.writeLog(
                    sharedPreferences, Action.BACKUP, info.packageName,
                    /* actionDescription */ "mode: " + optimizationMode);
            backupCount++;
        }

        writeBackupData(data, KEY_OPTIMIZATION_LIST, builder.toString());
        Log.d(TAG, String.format("backup getInstalledApplications():%d count=%d in %d/ms",
                applications.size(), backupCount, (System.currentTimeMillis() - timestamp)));
    }

    @VisibleForTesting
    int restoreOptimizationMode(byte[] dataBytes) {
        final long timestamp = System.currentTimeMillis();
        final String dataContent = new String(dataBytes, StandardCharsets.UTF_8);
        if (dataContent == null || dataContent.isEmpty()) {
            Log.w(TAG, "no data found in the restoreOptimizationMode()");
            return 0;
        }
        final String[] appConfigurations = dataContent.split(BatteryBackupHelper.DELIMITER);
        if (appConfigurations == null || appConfigurations.length == 0) {
            Log.w(TAG, "no data found from the split() processing");
            return 0;
        }
        int restoreCount = 0;
        for (int index = 0; index < appConfigurations.length; index++) {
            final String[] results = appConfigurations[index]
                    .split(BatteryBackupHelper.DELIMITER_MODE);
            // Example format: com.android.systemui:2 we should have length=2
            if (results == null || results.length != 2) {
                Log.w(TAG, "invalid raw data found:" + appConfigurations[index]);
                continue;
            }
            final String packageName = results[0];
            final int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName);
            // Ignores system/default apps.
            if (isSystemOrDefaultApp(packageName, uid)) {
                Log.w(TAG, "ignore from isSystemOrDefaultApp():" + packageName);
                continue;
            }
            @BatteryOptimizeUtils.OptimizationMode
            int optimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
            try {
                optimizationMode = Integer.parseInt(results[1]);
            } catch (NumberFormatException e) {
                Log.e(TAG, "failed to parse the optimization mode: "
                        + appConfigurations[index], e);
                continue;
            }
            restoreOptimizationMode(packageName, optimizationMode);
            restoreCount++;
        }
        Log.d(TAG, String.format("restoreOptimizationMode() count=%d in %d/ms",
                restoreCount, (System.currentTimeMillis() - timestamp)));
        return restoreCount;
    }

    private void performRestoreIfNeeded() {
        if (mOptimizationModeBytes == null || mOptimizationModeBytes.length == 0) {
            return;
        }
        final PowerUsageFeatureProvider provider = FeatureFactory.getFactory(mContext)
                .getPowerUsageFeatureProvider(mContext);
        if (!provider.isValidToRestoreOptimizationMode(mDeviceBuildInfoMap)) {
            return;
        }
        // Start to restore the app optimization mode data.
        final int restoreCount = restoreOptimizationMode(mOptimizationModeBytes);
        if (restoreCount > 0) {
            BatterySettingsMigrateChecker.verifyOptimizationModes(mContext);
        }
        mOptimizationModeBytes = null; // clear data
    }

    /** Dump the app optimization mode backup history data. */
    public static void dumpHistoricalData(Context context, PrintWriter writer) {
        BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(
                getSharedPreferences(context), writer);
    }

    static boolean isOwner() {
        return UserHandle.myUserId() == UserHandle.USER_SYSTEM;
    }

    static BatteryOptimizeUtils newBatteryOptimizeUtils(
            Context context, String packageName, BatteryOptimizeUtils testOptimizeUtils) {
        final int uid = BatteryUtils.getInstance(context).getPackageUid(packageName);
        if (uid == BatteryUtils.UID_NULL) {
            return null;
        }
        final BatteryOptimizeUtils batteryOptimizeUtils =
                testOptimizeUtils != null
                        ? testOptimizeUtils /*testing only*/
                        : new BatteryOptimizeUtils(context, uid, packageName);
        return batteryOptimizeUtils;
    }

    @VisibleForTesting
    static SharedPreferences getSharedPreferences(Context context) {
        return context.getSharedPreferences(
                BATTERY_OPTIMIZE_BACKUP_FILE_NAME, Context.MODE_PRIVATE);
    }

    private void restoreOptimizationMode(
            String packageName, @BatteryOptimizeUtils.OptimizationMode int mode) {
        final BatteryOptimizeUtils batteryOptimizeUtils =
                newBatteryOptimizeUtils(mContext, packageName, mBatteryOptimizeUtils);
        if (batteryOptimizeUtils == null) {
            return;
        }
        batteryOptimizeUtils.setAppUsageState(
                mode, BatteryOptimizeHistoricalLogEntry.Action.RESTORE);
        Log.d(TAG, String.format("restore:%s mode=%d", packageName, mode));
    }

    // Provides an opportunity to inject mock IDeviceIdleController for testing.
    private IDeviceIdleController getIDeviceIdleController() {
        if (mIDeviceIdleController != null) {
            return mIDeviceIdleController;
        }
        mIDeviceIdleController = IDeviceIdleController.Stub.asInterface(
                ServiceManager.getService(DEVICE_IDLE_SERVICE));
        return mIDeviceIdleController;
    }

    private IPackageManager getIPackageManager() {
        if (mIPackageManager != null) {
            return mIPackageManager;
        }
        mIPackageManager = AppGlobals.getPackageManager();
        return mIPackageManager;
    }

    private PowerAllowlistBackend getPowerAllowlistBackend() {
        if (mPowerAllowlistBackend != null) {
            return mPowerAllowlistBackend;
        }
        mPowerAllowlistBackend = PowerAllowlistBackend.getInstance(mContext);
        return mPowerAllowlistBackend;
    }

    private boolean isSystemOrDefaultApp(String packageName, int uid) {
        return BatteryOptimizeUtils.isSystemOrDefaultApp(
                getPowerAllowlistBackend(), packageName, uid);
    }

    private ArraySet<ApplicationInfo> getInstalledApplications() {
        if (mTestApplicationInfoList != null) {
            return mTestApplicationInfoList;
        }
        return BatteryOptimizeUtils.getInstalledApplications(mContext, getIPackageManager());
    }

    private void restoreBackupData(String dataKey, BackupDataInputStream data) {
        final byte[] dataBytes = getBackupData(dataKey, data);
        if (dataBytes == null || dataBytes.length == 0) {
            return;
        }
        final String dataContent = new String(dataBytes, StandardCharsets.UTF_8);
        mDeviceBuildInfoMap.put(dataKey, dataContent);
        Log.d(TAG, String.format("restore:%s:%s", dataKey, dataContent));
    }

    private static byte[] getBackupData(String dataKey, BackupDataInputStream data) {
        final int dataSize = data.size();
        final byte[] dataBytes = new byte[dataSize];
        try {
            data.read(dataBytes, 0 /*offset*/, dataSize);
        } catch (IOException e) {
            Log.e(TAG, "failed to getBackupData() " + dataKey, e);
            return null;
        }
        return dataBytes;
    }

    private static void writeBackupData(
            BackupDataOutput data, String dataKey, String dataContent) {
        if (dataContent == null || dataContent.isEmpty()) {
            return;
        }
        final byte[] dataContentBytes = dataContent.getBytes();
        try {
            data.writeEntityHeader(dataKey, dataContentBytes.length);
            data.writeEntityData(dataContentBytes, dataContentBytes.length);
        } catch (IOException e) {
            Log.e(TAG, "writeBackupData() is failed for " + dataKey, e);
        }
        Log.d(TAG, String.format("backup:%s:%s", dataKey, dataContent));
    }
}
