/*
 * 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 android.content.pm;

import android.Manifest;
import android.app.UiAutomation;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.compatibility.common.util.AdoptShellPermissionsRule;
import com.android.cts.install.lib.Install;
import com.android.cts.install.lib.InstallUtils;
import com.android.cts.install.lib.LocalIntentSender;
import com.android.cts.install.lib.TestApp;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class PackageInstallerBenchmark {
    private static final String TAG = "PackageInstallerBenchmark";

    @Rule
    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();

    /**
     * This rule adopts the Shell process permissions, needed because INSTALL_PACKAGES
     * and DELETE_PACKAGES are privileged permission.
     */
    @Rule
    public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
            InstrumentationRegistry.getInstrumentation().getUiAutomation(),
            Manifest.permission.INSTALL_PACKAGES,
            Manifest.permission.DELETE_PACKAGES);

    private static class SessionCallback extends PackageInstaller.SessionCallback {
        private final List<Integer> mExpectedSessions;
        private final CountDownLatch mCountDownLatch;
        private final boolean mExpectedSuccess;

        SessionCallback(boolean expectedSuccess, List<Integer> expectedSessions,
                @NonNull CountDownLatch countDownLatch) {
            mExpectedSuccess = expectedSuccess;
            mCountDownLatch = countDownLatch;
            mExpectedSessions = expectedSessions;
        }

        @Override
        public void onCreated(int sessionId) { }

        @Override
        public void onBadgingChanged(int sessionId) { }

        @Override
        public void onActiveChanged(int sessionId, boolean active) { }

        @Override
        public void onProgressChanged(int sessionId, float progress) { }

        @Override
        public void onFinished(int sessionId, boolean success) {
            if (success == mExpectedSuccess && mExpectedSessions.contains(sessionId)) {
                mCountDownLatch.countDown();
            }
        }
    }

    private CountDownLatch mCountDownLatch;
    private SessionCallback mSessionCallback;
    private PackageInstaller mPackageInstaller;
    private Install mInstall;
    private HandlerThread mHandlerThread;
    private List<PackageInstaller.Session> mExpectedSessions;
    private List<Integer> mExpectedSessionIds;
    final LocalIntentSender mLocalIntentSender = new LocalIntentSender();
    private IntentSender mIntentSender;

    @Before
    public void setUp() throws IOException {
        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        mPackageInstaller =  context.getPackageManager().getPackageInstaller();
        mHandlerThread = new HandlerThread("PackageInstallerBenchmark");
        mHandlerThread.start();

        mIntentSender = mLocalIntentSender.getIntentSender();
    }

    @After
    public void tearDown() throws InterruptedException {
        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        context.unregisterReceiver(mLocalIntentSender);

        uninstall(false /* stop at fail */, TestApp.A, TestApp.B, TestApp.C);
        mHandlerThread.quitSafely();
    }

    private List<PackageInstaller.Session> createSinglePackageSessions(
            BenchmarkState state, boolean expectedResult, TestApp...testApps)
            throws IOException, InterruptedException {
        state.pauseTiming();
        uninstall(false /* stop at fail */, testApps);

        mExpectedSessions = new ArrayList<>();
        mExpectedSessionIds = new ArrayList<>();
        for (TestApp testApp : testApps) {
            mInstall = Install.single(testApp);
            final int expectedSessionId = mInstall.createSession();
            PackageInstaller.Session session =
                    InstallUtils.openPackageInstallerSession(expectedSessionId);
            Log.d(TAG, "createNewSession: session expectedSessionId = " + expectedSessionId);
            mExpectedSessions.add(session);
            mExpectedSessionIds.add(expectedSessionId);
        }

        mCountDownLatch = new CountDownLatch(mExpectedSessions.size());
        mSessionCallback = new SessionCallback(expectedResult, mExpectedSessionIds,
                mCountDownLatch);
        mPackageInstaller.registerSessionCallback(mSessionCallback,
                mHandlerThread.getThreadHandler());
        state.resumeTiming();
        return mExpectedSessions;
    }

    private List<PackageInstaller.Session> createMultiplePackageSessions(BenchmarkState state,
            boolean expectedSuccess, List<TestApp[]> testAppsList)
            throws IOException, InterruptedException {
        state.pauseTiming();
        mExpectedSessions = new ArrayList<>();
        mExpectedSessionIds = new ArrayList<>();
        for (TestApp[] testApps : testAppsList) {
            uninstall(false /* stop at fail */, testApps);

            mInstall = Install.multi(testApps);
            final int expectedSessionId = mInstall.createSession();
            PackageInstaller.Session session =
                    InstallUtils.openPackageInstallerSession(expectedSessionId);
            mExpectedSessions.add(session);
            mExpectedSessionIds.add(expectedSessionId);
        }

        mCountDownLatch = new CountDownLatch(mExpectedSessions.size());
        mSessionCallback = new SessionCallback(expectedSuccess, mExpectedSessionIds,
                mCountDownLatch);
        mPackageInstaller.registerSessionCallback(mSessionCallback,
                mHandlerThread.getThreadHandler());
        state.resumeTiming();
        return mExpectedSessions;
    }

    private void uninstall(boolean stopAtFail, TestApp...testApps) throws InterruptedException {
        String[] packageNames = new String[testApps.length];
        for (int i = 0; i < testApps.length; i++) {
            packageNames[i] = testApps[i].getPackageName();
        }
        uninstall(stopAtFail, packageNames);
    }

    private void uninstall(boolean stopAtFail, String...packageNames) throws InterruptedException {
        LocalIntentSender localIntentSender = new LocalIntentSender();
        IntentSender intentSender = localIntentSender.getIntentSender();
        for (String packageName : packageNames) {
            try {
                mPackageInstaller.uninstall(packageName, intentSender);
            } catch (IllegalArgumentException e) {
                continue;
            }
            Intent intent = localIntentSender.getResult();
            if (stopAtFail) {
                InstallUtils.assertStatusSuccess(intent);
            }
        }

        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        context.unregisterReceiver(localIntentSender);
    }

    private void uninstallSession(BenchmarkState state, String...packageNames)
            throws Exception {
        state.pauseTiming();
        uninstall(true /* stop at fail */, packageNames);
        mPackageInstaller.unregisterSessionCallback(mSessionCallback);
        executeShellCommand("pm gc");
        state.resumeTiming();
    }

    private static String executeShellCommand(String command) throws IOException {
        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
        final ParcelFileDescriptor stdout = uiAutomation.executeShellCommand(command);
        try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout);
             ByteArrayOutputStream result = new ByteArrayOutputStream()) {
            writeFullStream(inputStream, result);
            return result.toString("UTF-8");
        }
    }

    private static void writeFullStream(InputStream inputStream, OutputStream outputStream)
            throws IOException {
        final byte[] buffer = new byte[1024];
        int length;
        while ((length = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, length);
        }
    }

    @Test(timeout = 600_000L)
    public void commit_aSingleApkSession_untilFinishBenchmark() throws Exception {
        uninstall(false /* stop at fail */, TestApp.A);

        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            List<PackageInstaller.Session> sessions =
                    createSinglePackageSessions(state, true, TestApp.A1);

            for (PackageInstaller.Session session : sessions) {
                session.commit(mIntentSender);
            }
            mCountDownLatch.await(1, TimeUnit.MINUTES);

            uninstallSession(state, TestApp.A);
        }
    }

    @Test(timeout = 600_000L)
    public void commit_threeSingleApkSessions_untilFinishBenchmark() throws Exception {
        uninstall(false /* stop at fail */, TestApp.A, TestApp.B, TestApp.C);

        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            List<PackageInstaller.Session> sessions = createSinglePackageSessions(
                    state, true, TestApp.A1, TestApp.B1, TestApp.C1);

            for (PackageInstaller.Session session : sessions) {
                session.commit(mIntentSender);
            }
            mCountDownLatch.await(1, TimeUnit.MINUTES);

            uninstallSession(state, TestApp.A, TestApp.B, TestApp.C);
        }
    }

    @Test(timeout = 600_000L)
    public void commit_aMultiplePackagesSession_untilFinishBenchmark() throws Exception {
        uninstall(false /* stop at fail */, TestApp.A, TestApp.B, TestApp.C);

        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        final List<TestApp[]> multiPackageApps = new ArrayList<>();
        multiPackageApps.add(new TestApp[] {TestApp.A1, TestApp.B1, TestApp.C1});

        while (state.keepRunning()) {
            List<PackageInstaller.Session> sessions = createMultiplePackageSessions(
                    state, true, multiPackageApps);

            for (PackageInstaller.Session session : sessions) {
                session.commit(mIntentSender);
            }
            mCountDownLatch.await(1, TimeUnit.MINUTES);

            uninstallSession(state, TestApp.A, TestApp.B, TestApp.C);
        }
    }

    @Test(timeout = 600_000L)
    public void commit_threeMultiplePackageSessions_untilFinishBenchmark() throws Exception {
        uninstall(false /* stop at fail */, TestApp.A, TestApp.B, TestApp.C);

        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        final List<TestApp[]> multiPackageApps = new ArrayList<>();
        multiPackageApps.add(new TestApp[] {TestApp.A1});
        multiPackageApps.add(new TestApp[] {TestApp.B1});
        multiPackageApps.add(new TestApp[] {TestApp.C1});

        while (state.keepRunning()) {
            List<PackageInstaller.Session> sessions = createMultiplePackageSessions(
                    state, true, multiPackageApps);

            for (PackageInstaller.Session session : sessions) {
                session.commit(mIntentSender);
            }
            mCountDownLatch.await(1, TimeUnit.MINUTES);

            uninstallSession(state, TestApp.A, TestApp.B, TestApp.C);
        }
    }

    @Test(timeout = 600_000L)
    public void commit_aMultipleApksSession_untilFinishBenchmark() throws Exception {
        uninstall(false /* stop at fail */, TestApp.A);

        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            List<PackageInstaller.Session> sessions = createSinglePackageSessions(
                    state, true, TestApp.ASplit1);

            for (PackageInstaller.Session session : sessions) {
                session.commit(mIntentSender);
            }
            mCountDownLatch.await(1, TimeUnit.MINUTES);

            uninstallSession(state, TestApp.A);
        }
    }
}
