/*
**
** Copyright 2007, 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.packageinstaller;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;

/**
 * This is a utility class for defining some utility methods and constants
 * used in the package installer application.
 */
public class PackageUtil {
    private static final String LOG_TAG = "PackageInstaller";

    public static final String PREFIX="com.android.packageinstaller.";
    public static final String INTENT_ATTR_INSTALL_STATUS = PREFIX+"installStatus";
    public static final String INTENT_ATTR_APPLICATION_INFO=PREFIX+"applicationInfo";
    public static final String INTENT_ATTR_PERMISSIONS_LIST=PREFIX+"PermissionsList";
    //intent attribute strings related to uninstall
    public static final String INTENT_ATTR_PACKAGE_NAME=PREFIX+"PackageName";
    private static final String DOWNLOADS_AUTHORITY = "downloads";
    private static final String SPLIT_BASE_APK_END_WITH = "base.apk";

    /**
     * Utility method to get package information for a given {@link File}
     */
    @Nullable
    public static PackageInfo getPackageInfo(Context context, File sourceFile, int flags) {
        String filePath = sourceFile.getAbsolutePath();
        if (filePath.endsWith(SPLIT_BASE_APK_END_WITH)) {
            File dir = sourceFile.getParentFile();
            if (dir.listFiles().length > 1) {
                // split apks, use file directory to get archive info
                filePath = dir.getPath();
            }
        }
        try {
            return context.getPackageManager().getPackageArchiveInfo(filePath, flags);
        } catch (Exception ignored) {
            return null;
        }
    }

    public static View initSnippet(View snippetView, CharSequence label, Drawable icon) {
        ((ImageView)snippetView.findViewById(R.id.app_icon)).setImageDrawable(icon);
        ((TextView)snippetView.findViewById(R.id.app_name)).setText(label);
        return snippetView;
    }

    /**
     * Utility method to display a snippet of an installed application.
     * The content view should have been set on context before invoking this method.
     * appSnippet view should include R.id.app_icon and R.id.app_name
     * defined on it.
     *
     * @param pContext context of package that can load the resources
     * @param componentInfo ComponentInfo object whose resources are to be loaded
     * @param snippetView the snippet view
     */
    public static View initSnippetForInstalledApp(Context pContext,
            ApplicationInfo appInfo, View snippetView) {
        return initSnippetForInstalledApp(pContext, appInfo, snippetView, null);
    }

    /**
     * Utility method to display a snippet of an installed application.
     * The content view should have been set on context before invoking this method.
     * appSnippet view should include R.id.app_icon and R.id.app_name
     * defined on it.
     *
     * @param pContext context of package that can load the resources
     * @param componentInfo ComponentInfo object whose resources are to be loaded
     * @param snippetView the snippet view
     * @param UserHandle user that the app si installed for.
     */
    public static View initSnippetForInstalledApp(Context pContext,
            ApplicationInfo appInfo, View snippetView, UserHandle user) {
        final PackageManager pm = pContext.getPackageManager();
        Drawable icon = appInfo.loadIcon(pm);
        if (user != null) {
            icon = pContext.getPackageManager().getUserBadgedIcon(icon, user);
        }
        return initSnippet(
                snippetView,
                appInfo.loadLabel(pm),
                icon);
    }

    static final class AppSnippet implements Parcelable {
        @NonNull public CharSequence label;
        @NonNull public Drawable icon;
        public int iconSize;

        AppSnippet(@NonNull CharSequence label, @NonNull Drawable icon, Context context) {
            this.label = label;
            this.icon = icon;
            final ActivityManager am = context.getSystemService(ActivityManager.class);
            this.iconSize = am.getLauncherLargeIconSize();
        }

        private AppSnippet(Parcel in) {
            label = in.readString();
            byte[] b = in.readBlob();
            Bitmap bmp = BitmapFactory.decodeByteArray(b, 0, b.length);
            icon = new BitmapDrawable(Resources.getSystem(), bmp);
            iconSize = in.readInt();
        }

        @Override
        public String toString() {
            return "AppSnippet[" + label + " (has icon)]";
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            dest.writeString(label.toString());

            Bitmap bmp = getBitmapFromDrawable(icon);
            dest.writeBlob(getBytesFromBitmap(bmp));
            bmp.recycle();

            dest.writeInt(iconSize);
        }

        private Bitmap getBitmapFromDrawable(Drawable drawable) {
            // Create an empty bitmap with the dimensions of our drawable
            final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            // Associate it with a canvas. This canvas will draw the icon on the bitmap
            final Canvas canvas = new Canvas(bmp);
            // Draw the drawable in the canvas. The canvas will ultimately paint the drawable in the
            // bitmap held within
            drawable.draw(canvas);

            // Scale it down if the icon is too large
            if ((bmp.getWidth() > iconSize * 2) || (bmp.getHeight() > iconSize * 2)) {
                Bitmap scaledBitmap = Bitmap.createScaledBitmap(bmp, iconSize, iconSize, true);
                if (scaledBitmap != bmp) {
                    bmp.recycle();
                }
                return scaledBitmap;
            }
            return bmp;
        }

        private byte[] getBytesFromBitmap(Bitmap bmp) {
            ByteArrayOutputStream baos = null;
            try {
                baos = new ByteArrayOutputStream();
                bmp.compress(CompressFormat.PNG, 100, baos);
            } finally {
                try {
                    if (baos != null) {
                        baos.close();
                    }
                } catch (IOException e) {
                    Log.e(LOG_TAG, "ByteArrayOutputStream was not closed");
                }
            }
            return baos.toByteArray();
        }

        public static final Parcelable.Creator<AppSnippet> CREATOR = new Parcelable.Creator<>() {
            public AppSnippet createFromParcel(Parcel in) {
                return new AppSnippet(in);
            }

            public AppSnippet[] newArray(int size) {
                return new AppSnippet[size];
            }
        };
    }

    /**
     * Utility method to load application label
     *
     * @param pContext context of package that can load the resources
     * @param appInfo ApplicationInfo object of package whose resources are to be loaded
     * @param sourceFile File the package is in
     */
    public static AppSnippet getAppSnippet(
            Activity pContext, ApplicationInfo appInfo, File sourceFile) {
        final String archiveFilePath = sourceFile.getAbsolutePath();
        PackageManager pm = pContext.getPackageManager();
        appInfo.publicSourceDir = archiveFilePath;

        if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) {
            final File[] files = sourceFile.getParentFile().listFiles();
            final String[] splits = Arrays.stream(appInfo.splitNames)
                    .map(i -> findFilePath(files, i + ".apk"))
                    .filter(Objects::nonNull)
                    .toArray(String[]::new);

            appInfo.splitSourceDirs = splits;
            appInfo.splitPublicSourceDirs = splits;
        }

        CharSequence label = null;
        // Try to load the label from the package's resources. If an app has not explicitly
        // specified any label, just use the package name.
        if (appInfo.labelRes != 0) {
            try {
                label = appInfo.loadLabel(pm);
            } catch (Resources.NotFoundException e) {
            }
        }
        if (label == null) {
            label = (appInfo.nonLocalizedLabel != null) ?
                    appInfo.nonLocalizedLabel : appInfo.packageName;
        }
        Drawable icon = null;
        // Try to load the icon from the package's resources. If an app has not explicitly
        // specified any resource, just use the default icon for now.
        try {
            if (appInfo.icon != 0) {
                try {
                    icon = appInfo.loadIcon(pm);
                } catch (Resources.NotFoundException e) {
                }
            }
            if (icon == null) {
                icon = pContext.getPackageManager().getDefaultActivityIcon();
            }
        } catch (OutOfMemoryError e) {
            Log.i(LOG_TAG, "Could not load app icon", e);
        }
        return new PackageUtil.AppSnippet(label, icon, pContext);
    }

    private static String findFilePath(File[] files, String postfix) {
        for (File file : files) {
            final String path = file.getAbsolutePath();
            if (path.endsWith(postfix)) {
                return path;
            }
        }
        return null;
    }

    /**
     * Get the maximum target sdk for a UID.
     *
     * @param context The context to use
     * @param uid The UID requesting the install/uninstall
     *
     * @return The maximum target SDK or -1 if the uid does not match any packages.
     */
    static int getMaxTargetSdkVersionForUid(@NonNull Context context, int uid) {
        PackageManager pm = context.getPackageManager();
        final String[] packages = pm.getPackagesForUid(uid);
        int targetSdkVersion = -1;
        if (packages != null) {
            for (String packageName : packages) {
                try {
                    ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
                    targetSdkVersion = Math.max(targetSdkVersion, info.targetSdkVersion);
                } catch (PackageManager.NameNotFoundException e) {
                    // Ignore and try the next package
                }
            }
        }
        return targetSdkVersion;
    }


    /**
     * Quietly close a closeable resource (e.g. a stream or file). The input may already
     * be closed and it may even be null.
     */
    static void safeClose(Closeable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException ioe) {
                // Catch and discard the error
            }
        }
    }

    /**
     * A simple error dialog showing a message
     */
    public static class SimpleErrorDialog extends DialogFragment {
        private static final String MESSAGE_KEY =
                SimpleErrorDialog.class.getName() + "MESSAGE_KEY";

        static SimpleErrorDialog newInstance(@StringRes int message) {
            SimpleErrorDialog dialog = new SimpleErrorDialog();

            Bundle args = new Bundle();
            args.putInt(MESSAGE_KEY, message);
            dialog.setArguments(args);

            return dialog;
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            return new AlertDialog.Builder(getActivity())
                    .setMessage(getArguments().getInt(MESSAGE_KEY))
                    .setPositiveButton(R.string.ok, (dialog, which) -> getActivity().finish())
                    .create();
        }

        @Override
        public void onCancel(DialogInterface dialog) {
            getActivity().setResult(Activity.RESULT_CANCELED);
            getActivity().finish();
        }
    }

    /**
     * Determines if the UID belongs to the system downloads provider and returns the
     * {@link ApplicationInfo} of the provider
     *
     * @param uid UID of the caller
     * @return {@link ApplicationInfo} of the provider if a downloads provider exists,
     *          it is a system app, and its UID matches with the passed UID, null otherwise.
     */
    public static ApplicationInfo getSystemDownloadsProviderInfo(PackageManager pm, int uid) {
        final ProviderInfo providerInfo = pm.resolveContentProvider(
                DOWNLOADS_AUTHORITY, 0);
        if (providerInfo == null) {
            // There seems to be no currently enabled downloads provider on the system.
            return null;
        }
        ApplicationInfo appInfo = providerInfo.applicationInfo;
        if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && uid == appInfo.uid) {
            return appInfo;
        }
        return null;
    }
}
