/*
 * 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.tv.settings.users;

import com.android.tv.settings.R;
import com.android.tv.settings.dialog.DialogFragment;
import com.android.tv.settings.dialog.DialogFragment.Action;

import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.content.Intent.ShortcutIconResource;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;

import com.android.tv.settings.util.UriUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

class AppLoadingTask extends AsyncTask<Void, Void, List<AppLoadingTask.SelectableAppInfo>> {

    interface Listener {
        void onPackageEnableChanged(String packageName, boolean enabled);

        void onActionsLoaded(ArrayList<Action> actions);
    }

    private static final boolean DEBUG = false;
    private static final String TAG = "RestrictedProfile";

    private final Context mContext;
    private final int mUserId;
    private final boolean mNewUser;
    private final PackageManager mPackageManager;
    private final IPackageManager mIPackageManager;
    private final Listener mListener;
    private final PackageInfo mSysPackageInfo;
    private final HashMap<String, Boolean> mSelectedPackages = new HashMap<String, Boolean>();
    private boolean mFirstTime = true;

    /**
     * Loads the list of activities that the user can enable or disable in a restricted profile.
     *
     * @param context context for querying the list of activities.
     * @param userId the user ID of the user whose apps should be listed.
     * @param newUser true if this is a newly create user.
     * @param iPackageManager used to get application info.
     * @param listener listener for package enable state changes.
     */
    AppLoadingTask(Context context, int userId, boolean newUser, IPackageManager iPackageManager,
            Listener listener) {
        mContext = context;
        mUserId = userId;
        mNewUser = newUser;
        mPackageManager = context.getPackageManager();
        mIPackageManager = iPackageManager;
        mListener = listener;
        PackageInfo sysPackageInfo = null;
        try {
            sysPackageInfo = mPackageManager.getPackageInfo("android",
                    PackageManager.GET_SIGNATURES);
        } catch (NameNotFoundException nnfe) {
            Log.wtf(TAG, "Failed to get package signatures!");
        }
        mSysPackageInfo = sysPackageInfo;
    }

    @Override
    protected List<SelectableAppInfo> doInBackground(Void... params) {
        return fetchAndMergeApps();
    }

    @Override
    protected void onPostExecute(List<SelectableAppInfo> visibleApps) {
        populateApps(visibleApps);
    }

    private void populateApps(List<SelectableAppInfo> visibleApps) {
        ArrayList<Action> actions = new ArrayList<Action>();
        Intent restrictionsIntent = new Intent(Intent.ACTION_GET_RESTRICTION_ENTRIES);
        List<ResolveInfo> receivers = mPackageManager.queryBroadcastReceivers(restrictionsIntent,
                0);
        for (SelectableAppInfo app : visibleApps) {
            String packageName = app.packageName;
            if (packageName == null) {
                if (DEBUG) {
                    Log.d(TAG, "App has no package name: " + app.appName);
                }
                continue;
            }
            final boolean isSettingsApp = packageName.equals(mContext.getPackageName());
            final boolean hasSettings = resolveInfoListHasPackage(receivers, packageName);
            boolean isAllowed = false;
            String controllingActivity = null;
            if (app.masterEntry != null) {
                controllingActivity = app.masterEntry.activityName.toString();
            }
            boolean hasCustomizableRestrictions = ((hasSettings || isSettingsApp)
                    && app.masterEntry == null);
            PackageInfo pi = null;
            try {
                pi = mIPackageManager.getPackageInfo(packageName,
                        PackageManager.GET_UNINSTALLED_PACKAGES
                        | PackageManager.GET_SIGNATURES, mUserId);
            } catch (RemoteException e) {
            }
            boolean canBeEnabledDisabled = true;
            if (pi != null && (pi.requiredForAllUsers || isPlatformSigned(pi))) {
                isAllowed = true;
                canBeEnabledDisabled = false;
                // If the app is required and has no restrictions, skip showing it
                if (!hasSettings && !isSettingsApp) {
                    if (DEBUG) {
                        Log.d(TAG, "App is required and has no settings: " + app.appName);
                    }
                    continue;
                }
                // Get and populate the defaults, since the user is not going to be
                // able to toggle this app ON (it's ON by default and immutable).
                // Only do this for restricted profiles, not single-user restrictions
                // Also don't do this for slave icons
            } else if (!mNewUser && isAppEnabledForUser(pi)) {
                isAllowed = true;
            }
            boolean availableForRestrictedProfile = true;
            if (pi.requiredAccountType != null && pi.restrictedAccountType == null) {
                availableForRestrictedProfile = false;
                isAllowed = false;
                canBeEnabledDisabled = false;
            }
            boolean canSeeRestrictedAccounts = pi.restrictedAccountType != null;
            if (app.masterEntry != null) {
                canBeEnabledDisabled = false;
                isAllowed = mSelectedPackages.get(packageName);
            }
            onPackageEnableChanged(packageName, isAllowed);
            if (DEBUG) {
                Log.d(TAG, "Adding action for: " + app.appName + " has restrictions: "
                        + hasCustomizableRestrictions);
            }
            actions.add(UserAppRestrictionsDialogFragment.createAction(mContext, packageName,
                    app.activityName.toString(), getAppIconUri(mContext, app.info, app.iconRes),
                    canBeEnabledDisabled, isAllowed, hasCustomizableRestrictions,
                    canSeeRestrictedAccounts, availableForRestrictedProfile, controllingActivity));
        }
        mListener.onActionsLoaded(actions);
        // If this is the first time for a new profile, install/uninstall default apps for
        // profile
        // to avoid taking the hit in onPause(), which can cause race conditions on user switch.
        if (mNewUser && mFirstTime) {
            mFirstTime = false;
            UserAppRestrictionsDialogFragment.applyUserAppsStates(mSelectedPackages, actions,
                    mIPackageManager, mUserId);
        }
    }

    private void onPackageEnableChanged(String packageName, boolean enabled) {
        mListener.onPackageEnableChanged(packageName, enabled);
        mSelectedPackages.put(packageName, enabled);
    }

    private boolean resolveInfoListHasPackage(List<ResolveInfo> receivers, String packageName) {
        for (ResolveInfo info : receivers) {
            if (info.activityInfo.packageName.equals(packageName)) {
                return true;
            }
        }
        return false;
    }

    private List<SelectableAppInfo> fetchAndMergeApps() {
        List<SelectableAppInfo> visibleApps = new ArrayList<SelectableAppInfo>();

        // Find all pre-installed input methods that are marked as default and add them to an
        // exclusion list so that they aren't presented to the user for toggling. Don't add
        // non-default ones, as they may include other stuff that we don't need to auto-include.
        final HashSet<String> defaultSystemImes = getDefaultSystemImes();

        // Add Settings
        try {
            visibleApps.add(new SelectableAppInfo(mPackageManager,
                    mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)));
        } catch (NameNotFoundException nnfe) {
            Log.e(TAG, "Couldn't add settings item to list!", nnfe);
        }

        // Add leanback launchers
        Intent leanbackLauncherIntent = new Intent(Intent.ACTION_MAIN);
        leanbackLauncherIntent.addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER);
        addSystemApps(visibleApps, leanbackLauncherIntent, defaultSystemImes, mUserId);

        // Add widgets
        Intent widgetIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        addSystemApps(visibleApps, widgetIntent, defaultSystemImes, mUserId);

        List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications(
                PackageManager.GET_UNINSTALLED_PACKAGES);
        addNonSystemApps(installedApps, true, visibleApps);

        // Get the list of apps already installed for the user
        try {
            List<ApplicationInfo> userApps = mIPackageManager.getInstalledApplications(
                    PackageManager.GET_UNINSTALLED_PACKAGES, mUserId).getList();
            addNonSystemApps(userApps, false, visibleApps);
        } catch (RemoteException re) {
        }

        // Sort the list of visible apps
        Collections.sort(visibleApps, new AppLabelComparator());

        // Remove dupes
        Set<String> dedupPackageSet = new HashSet<String>();
        for (int i = visibleApps.size() - 1; i >= 0; i--) {
            SelectableAppInfo info = visibleApps.get(i);
            if (DEBUG) {
                Log.i(TAG, info.toString());
            }
            String both = info.packageName + "+" + info.activityName;
            if (!TextUtils.isEmpty(info.packageName)
                    && !TextUtils.isEmpty(info.activityName)
                    && dedupPackageSet.contains(both)) {
                if (DEBUG) {
                    Log.d(TAG, "Removing app: " + info.appName);
                }
                visibleApps.remove(i);
            } else {
                dedupPackageSet.add(both);
            }
        }

        // Establish master/slave relationship for entries that share a package name
        HashMap<String, SelectableAppInfo> packageMap = new HashMap<String,
                SelectableAppInfo>();
        for (SelectableAppInfo info : visibleApps) {
            if (packageMap.containsKey(info.packageName)) {
                info.masterEntry = packageMap.get(info.packageName);
            } else {
                packageMap.put(info.packageName, info);
            }
        }
        return visibleApps;
    }

    private void addNonSystemApps(List<ApplicationInfo> apps, boolean disableSystemApps,
            List<SelectableAppInfo> visibleApps) {
        if (apps == null) {
            return;
        }

        for (ApplicationInfo app : apps) {
            // If it's not installed, skip
            if ((app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
                continue;
            }

            if ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0
                    && (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0) {
                // Downloaded app
                visibleApps.add(new SelectableAppInfo(mPackageManager, app));
            } else if (disableSystemApps) {
                try {
                    PackageInfo pi = mPackageManager.getPackageInfo(app.packageName, 0);
                    // If it's a system app that requires an account and doesn't see restricted
                    // accounts, mark for removal. It might get shown in the UI if it has an
                    // icon but will still be marked as false and immutable.
                    if (pi.requiredAccountType != null && pi.restrictedAccountType == null) {
                        onPackageEnableChanged(app.packageName, false);
                    }
                } catch (NameNotFoundException re) {
                }
            }
        }
    }

    static class SelectableAppInfo {
        private final String packageName;
        private final CharSequence appName;
        private final CharSequence activityName;
        private final ApplicationInfo info;
        private final int iconRes;
        private SelectableAppInfo masterEntry;

        SelectableAppInfo(PackageManager packageManager, ResolveInfo resolveInfo) {
            packageName = resolveInfo.activityInfo.packageName;
            appName = resolveInfo.activityInfo.applicationInfo.loadLabel(packageManager);
            CharSequence label = resolveInfo.activityInfo.loadLabel(packageManager);
            activityName = (label != null) ? label : appName;
            int activityIconRes = getIconResource(resolveInfo.activityInfo);
            info = resolveInfo.activityInfo.applicationInfo;
            iconRes = activityIconRes != 0 ? activityIconRes
                    : getIconResource(resolveInfo.activityInfo.applicationInfo);
        }

        SelectableAppInfo(PackageManager packageManager, ApplicationInfo applicationInfo) {
            packageName = applicationInfo.packageName;
            appName = applicationInfo.loadLabel(packageManager);
            activityName = appName;
            info = applicationInfo;
            iconRes = getIconResource(applicationInfo);
        }

        @Override
        public String toString() {
            return packageName + ": appName=" + appName + "; activityName=" + activityName
                    + "; masterEntry=" + masterEntry;
        }

        private int getIconResource(PackageItemInfo packageItemInfo) {
            if (packageItemInfo.banner != 0) {
                return packageItemInfo.banner;
            }
            if (packageItemInfo.logo != 0) {
                return packageItemInfo.logo;
            }
            return packageItemInfo.icon;
        }
    }

    private static class AppLabelComparator implements Comparator<SelectableAppInfo> {

        @Override
        public int compare(SelectableAppInfo lhs, SelectableAppInfo rhs) {
            String lhsLabel = lhs.activityName.toString();
            String rhsLabel = rhs.activityName.toString();
            return lhsLabel.toLowerCase().compareTo(rhsLabel.toLowerCase());
        }
    }

    /**
     * Find all pre-installed input methods that are marked as default and add them to an exclusion
     * list so that they aren't presented to the user for toggling. Don't add non-default ones, as
     * they may include other stuff that we don't need to auto-include.
     *
     * @return the set of default system imes
     */
    private HashSet<String> getDefaultSystemImes() {
        HashSet<String> defaultSystemImes = new HashSet<String>();
        InputMethodManager imm = (InputMethodManager)
                mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
        List<InputMethodInfo> imis = imm.getInputMethodList();
        for (InputMethodInfo imi : imis) {
            try {
                if (imi.isDefault(mContext) && isSystemPackage(imi.getPackageName())) {
                    defaultSystemImes.add(imi.getPackageName());
                }
            } catch (Resources.NotFoundException rnfe) {
                // Not default
            }
        }
        return defaultSystemImes;
    }

    private boolean isSystemPackage(String packageName) {
        try {
            final PackageInfo pi = mPackageManager.getPackageInfo(packageName, 0);
            if (pi.applicationInfo == null)
                return false;
            final int flags = pi.applicationInfo.flags;
            if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0
                    || (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
                return true;
            }
        } catch (NameNotFoundException nnfe) {
            // Missing package?
        }
        return false;
    }

    /**
     * Add system apps that match an intent to the list, excluding any packages in the exclude list.
     *
     * @param visibleApps list of apps to append the new list to
     * @param intent the intent to match
     * @param excludePackages the set of package names to be excluded, since they're required
     */
    private void addSystemApps(List<SelectableAppInfo> visibleApps, Intent intent,
            Set<String> excludePackages, int userId) {
        final PackageManager pm = mPackageManager;
        List<ResolveInfo> launchableApps = pm.queryIntentActivities(intent,
                PackageManager.GET_DISABLED_COMPONENTS
                | PackageManager.GET_UNINSTALLED_PACKAGES);
        for (ResolveInfo app : launchableApps) {
            if (app.activityInfo != null && app.activityInfo.applicationInfo != null) {
                final String packageName = app.activityInfo.packageName;
                int flags = app.activityInfo.applicationInfo.flags;
                if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0
                        || (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
                    if (DEBUG) {
                        Log.d(TAG, "Found system app: "
                                + app.activityInfo.applicationInfo.loadLabel(pm));
                    }
                    // System app
                    // Skip excluded packages
                    if (excludePackages.contains(packageName)) {
                        if (DEBUG) {
                            Log.d(TAG, "App is an excluded ime, not adding: "
                                    + app.activityInfo.applicationInfo.loadLabel(pm));
                        }
                        continue;
                    }
                    int enabled = pm.getApplicationEnabledSetting(packageName);
                    if (enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
                            || enabled == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
                        // Check if the app is already enabled for the target user
                        ApplicationInfo targetUserAppInfo = getAppInfoForUser(packageName,
                                0, userId);
                        if (targetUserAppInfo == null
                                || (targetUserAppInfo.flags & ApplicationInfo.FLAG_INSTALLED)
                                        == 0) {
                            if (DEBUG) {
                                Log.d(TAG, "App is already something, not adding: "
                                        + app.activityInfo.applicationInfo.loadLabel(pm));
                            }
                            continue;
                        }
                    }

                    if (DEBUG) {
                        Log.d(TAG, "Adding system app: "
                                + app.activityInfo.applicationInfo.loadLabel(pm));
                    }
                    visibleApps.add(new SelectableAppInfo(pm, app));
                }
            }
        }
    }

    private ApplicationInfo getAppInfoForUser(String packageName, int flags, int userId) {
        try {
            ApplicationInfo targetUserAppInfo = mIPackageManager.getApplicationInfo(packageName,
                    flags,
                    userId);
            return targetUserAppInfo;
        } catch (RemoteException re) {
            return null;
        }
    }

    private boolean isPlatformSigned(PackageInfo pi) {
        return (pi != null && pi.signatures != null &&
                mSysPackageInfo.signatures[0].equals(pi.signatures[0]));
    }

    private boolean isAppEnabledForUser(PackageInfo pi) {
        if (pi == null)
            return false;
        final int flags = pi.applicationInfo.flags;
        // Return true if it is installed and not hidden
        return ((flags & ApplicationInfo.FLAG_INSTALLED) != 0
                && (flags & ApplicationInfo.FLAG_HIDDEN) == 0);
    }

    private static Uri getAppIconUri(Context context, ApplicationInfo info, int iconRes) {
        String iconUri = null;
        if (iconRes != 0) {
            try {
                Resources resources = context.getPackageManager()
                        .getResourcesForApplication(info);
                ShortcutIconResource iconResource = new ShortcutIconResource();
                iconResource.packageName = info.packageName;
                iconResource.resourceName = resources.getResourceName(iconRes);
                iconUri = UriUtils.getShortcutIconResourceUri(iconResource).toString();
            } catch (Exception e1) {
                Log.w("AppsBrowseInfo", e1.toString());
            }
        } else {
            iconUri = UriUtils.getAndroidResourceUri(Resources.getSystem(),
                    com.android.internal.R.drawable.sym_def_app_icon);
        }

        if (iconUri == null) {
            iconUri = UriUtils.getAndroidResourceUri(context.getResources(),
                    com.android.internal.R.drawable.sym_app_on_sd_unavailable_icon);
        }
        return Uri.parse(iconUri);
    }
}
