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

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;

import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.SessionCommitReceiver;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.FileLog;
import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

/**
 * Utility class to tracking install sessions
 */
public class InstallSessionHelper {

    @NonNull
    private static final String LOG = "InstallSessionHelper";

    // Set<String> of session ids of promise icons that have been added to the home screen
    // as FLAG_PROMISE_NEW_INSTALLS.
    @NonNull
    public static final String PROMISE_ICON_IDS = "promise_icon_ids";

    private static final boolean DEBUG = false;

    @NonNull
    public static final MainThreadInitializedObject<InstallSessionHelper> INSTANCE =
            new MainThreadInitializedObject<>(InstallSessionHelper::new);

    @Nullable
    private final LauncherApps mLauncherApps;

    @NonNull
    private final Context mAppContext;

    @NonNull
    private final PackageInstaller mInstaller;

    @NonNull
    private final HashMap<String, Boolean> mSessionVerifiedMap = new HashMap<>();

    @Nullable
    private IntSet mPromiseIconIds;

    public InstallSessionHelper(@NonNull final Context context) {
        mInstaller = context.getPackageManager().getPackageInstaller();
        mAppContext = context.getApplicationContext();
        mLauncherApps = context.getSystemService(LauncherApps.class);
    }

    @WorkerThread
    @NonNull
    private IntSet getPromiseIconIds() {
        Preconditions.assertWorkerThread();
        if (mPromiseIconIds != null) {
            return mPromiseIconIds;
        }
        mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString(
                LauncherPrefs.get(mAppContext).get(LauncherPrefs.PROMISE_ICON_IDS)));

        IntArray existingIds = new IntArray();
        for (SessionInfo info : getActiveSessions().values()) {
            existingIds.add(info.getSessionId());
        }
        IntArray idsToRemove = new IntArray();

        for (int i = mPromiseIconIds.size() - 1; i >= 0; --i) {
            if (!existingIds.contains(mPromiseIconIds.getArray().get(i))) {
                idsToRemove.add(mPromiseIconIds.getArray().get(i));
            }
        }
        for (int i = idsToRemove.size() - 1; i >= 0; --i) {
            mPromiseIconIds.getArray().removeValue(idsToRemove.get(i));
        }
        return mPromiseIconIds;
    }

    @NonNull
    public HashMap<PackageUserKey, SessionInfo> getActiveSessions() {
        HashMap<PackageUserKey, SessionInfo> activePackages = new HashMap<>();
        for (SessionInfo info : getAllVerifiedSessions()) {
            activePackages.put(new PackageUserKey(info.getAppPackageName(), getUserHandle(info)),
                    info);
        }
        return activePackages;
    }

    @Nullable
    public SessionInfo getActiveSessionInfo(UserHandle user, String pkg) {
        for (SessionInfo info : getAllVerifiedSessions()) {
            boolean match = pkg.equals(info.getAppPackageName());
            if (Utilities.ATLEAST_Q && !user.equals(getUserHandle(info))) {
                match = false;
            }
            if (match) {
                return info;
            }
        }
        return null;
    }

    private void updatePromiseIconPrefs() {
        LauncherPrefs.get(mAppContext).put(LauncherPrefs.PROMISE_ICON_IDS,
                getPromiseIconIds().getArray().toConcatString());
    }

    @Nullable
    SessionInfo getVerifiedSessionInfo(final int sessionId) {
        return verify(mInstaller.getSessionInfo(sessionId));
    }

    @Nullable
    private SessionInfo verify(@Nullable final SessionInfo sessionInfo) {
        if (sessionInfo == null
                || sessionInfo.getInstallerPackageName() == null
                || TextUtils.isEmpty(sessionInfo.getAppPackageName())) {
            if (TestProtocol.sDebugTracing) {
                Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " verify"
                        + ", info=" + (sessionInfo == null)
                        + ", info install name" + (sessionInfo == null
                                ? null
                                : sessionInfo.getInstallerPackageName())
                        + ", empty pkg name" + TextUtils.isEmpty((sessionInfo == null
                                ? null
                                : sessionInfo.getAppPackageName())));
            }
            return null;
        }
        return isTrustedPackage(sessionInfo.getInstallerPackageName(), getUserHandle(sessionInfo))
                ? sessionInfo : null;
    }

    /**
     * Returns true if the provided packageName can be trusted for user configurations
     */
    public boolean isTrustedPackage(String pkg, UserHandle user) {
        synchronized (mSessionVerifiedMap) {
            if (!mSessionVerifiedMap.containsKey(pkg)) {
                boolean hasSystemFlag = new PackageManagerHelper(mAppContext).getApplicationInfo(
                        pkg, user, ApplicationInfo.FLAG_SYSTEM) != null;
                mSessionVerifiedMap.put(pkg, DEBUG || hasSystemFlag);
            }
        }
        return mSessionVerifiedMap.get(pkg);
    }

    @NonNull
    public List<SessionInfo> getAllVerifiedSessions() {
        List<SessionInfo> list = new ArrayList<>(Utilities.ATLEAST_Q
                ? Objects.requireNonNull(mLauncherApps).getAllPackageInstallerSessions()
                : mInstaller.getAllSessions());
        Iterator<SessionInfo> it = list.iterator();
        while (it.hasNext()) {
            if (verify(it.next()) == null) {
                it.remove();
            }
        }
        return list;
    }

    /**
     * Attempt to restore workspace layout if the session is triggered due to device restore.
     */
    public boolean restoreDbIfApplicable(@NonNull final SessionInfo info) {
        if (!FeatureFlags.ENABLE_DATABASE_RESTORE.get()) {
            return false;
        }
        if (isRestore(info)) {
            LauncherSettings.Settings.call(mAppContext.getContentResolver(),
                    LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE);
            return true;
        }
        return false;
    }

    @RequiresApi(26)
    private static boolean isRestore(@NonNull final SessionInfo info) {
        return info.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_RESTORE;
    }

    @WorkerThread
    public boolean promiseIconAddedForId(final int sessionId) {
        return getPromiseIconIds().contains(sessionId);
    }

    @WorkerThread
    public void removePromiseIconId(final int sessionId) {
        if (promiseIconAddedForId(sessionId)) {
            getPromiseIconIds().getArray().removeValue(sessionId);
            updatePromiseIconPrefs();
        }
    }

    /**
     * Add a promise app icon to the workspace iff:
     * - The settings for it are enabled
     * - The user installed the app
     * - There is an app icon and label (For apps with no launching activity, no icon is provided).
     * - The app is not already installed
     * - A promise icon for the session has not already been created
     */
    @WorkerThread
    void tryQueuePromiseAppIcon(@Nullable final PackageInstaller.SessionInfo sessionInfo) {
        if (TestProtocol.sDebugTracing) {
            Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " tryQueuePromiseAppIcon"
                    + ", SessionCommitReceiveEnabled" + SessionCommitReceiver.isEnabled(mAppContext)
                    + ", verifySessionInfo(sessionInfo)=" + verifySessionInfo(sessionInfo)
                    + ", !promiseIconAdded=" + (sessionInfo != null
                    && !promiseIconAddedForId(sessionInfo.getSessionId())));
        }
        if (SessionCommitReceiver.isEnabled(mAppContext)
                && verifySessionInfo(sessionInfo)
                && !promiseIconAddedForId(sessionInfo.getSessionId())) {
            FileLog.d(LOG, "Adding package name to install queue: "
                    + sessionInfo.getAppPackageName());

            ItemInstallQueue.INSTANCE.get(mAppContext)
                    .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));

            getPromiseIconIds().add(sessionInfo.getSessionId());
            updatePromiseIconPrefs();
        }
    }

    public boolean verifySessionInfo(@Nullable final PackageInstaller.SessionInfo sessionInfo) {
        if (TestProtocol.sDebugTracing) {
            boolean appNotInstalled = sessionInfo == null
                    || !new PackageManagerHelper(mAppContext)
                    .isAppInstalled(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
            boolean labelNotEmpty = sessionInfo != null
                    && !TextUtils.isEmpty(sessionInfo.getAppLabel());
            Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " verifySessionInfo"
                    + ", verify(sessionInfo)=" + verify(sessionInfo)
                    + ", reason=" + (sessionInfo == null ? null : sessionInfo.getInstallReason())
                    + ", PackageManager.INSTALL_REASON_USER=" + PackageManager.INSTALL_REASON_USER
                    + ", hasIcon=" + (sessionInfo != null && sessionInfo.getAppIcon() != null)
                    + ", label is ! empty=" + labelNotEmpty
                    + " +, app not installed="  + appNotInstalled);
        }
        return verify(sessionInfo) != null
                && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
                && sessionInfo.getAppIcon() != null
                && !TextUtils.isEmpty(sessionInfo.getAppLabel())
                && !new PackageManagerHelper(mAppContext).isAppInstalled(
                        sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
    }

    public InstallSessionTracker registerInstallTracker(
            @Nullable final InstallSessionTracker.Callback callback) {
        InstallSessionTracker tracker = new InstallSessionTracker(
                this, callback, mInstaller, mLauncherApps);
        tracker.register();
        return tracker;
    }

    public static UserHandle getUserHandle(@NonNull final SessionInfo info) {
        return Utilities.ATLEAST_Q ? info.getUser() : Process.myUserHandle();
    }
}
