/*
 * Copyright (C) 2019 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.settingslib.drawer;

import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings.Global;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.VisibleForTesting;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Utils is a helper class that contains profile key, meta data, settings action
 * and static methods for get icon or text from uri.
 */
public class TileUtils {

    private static final boolean DEBUG_TIMING = false;

    private static final String LOG_TAG = "TileUtils";
    @VisibleForTesting
    static final String SETTING_PKG = "com.android.settings";

    /**
     * Settings will search for system activities of this action and add them as a top level
     * settings tile using the following parameters.
     *
     * <p>A category must be specified in the meta-data for the activity named
     * {@link #EXTRA_CATEGORY_KEY}
     *
     * <p>The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE}
     * otherwise the label for the activity will be used.
     *
     * <p>The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON}
     * otherwise the icon for the activity will be used.
     *
     * <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY}
     */
    public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";

    /**
     * @See {@link #EXTRA_SETTINGS_ACTION}.
     */
    public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";

    /** Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities. */
    private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";

    private static final String OPERATOR_SETTINGS =
            "com.android.settings.OPERATOR_APPLICATION_SETTING";

    private static final String OPERATOR_DEFAULT_CATEGORY =
            "com.android.settings.category.wireless";

    private static final String MANUFACTURER_SETTINGS =
            "com.android.settings.MANUFACTURER_APPLICATION_SETTING";

    private static final String MANUFACTURER_DEFAULT_CATEGORY =
            "com.android.settings.category.device";

    /**
     * The key used to get the category from metadata of activities of action
     * {@link #EXTRA_SETTINGS_ACTION}
     * The value must be from {@link CategoryKey}.
     */
    static final String EXTRA_CATEGORY_KEY = "com.android.settings.category";

    /** The key used to get the package name of the icon resource for the preference. */
    static final String EXTRA_PREFERENCE_ICON_PACKAGE = "com.android.settings.icon_package";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml
     * to specify the key that should be used for the preference.
     */
    public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint";

    /**
     * Name of the meta-data item that can be set in the AndroidManifest.xml or in the content
     * provider to specify the key of a group / category where this preference belongs to.
     */
    public static final String META_DATA_PREFERENCE_GROUP_KEY = "com.android.settings.group_key";

    /**
     * Order of the item that should be displayed on screen. Bigger value items displays closer on
     * top.
     */
    public static final String META_DATA_KEY_ORDER = "com.android.settings.order";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml
     * to specify the icon that should be displayed for the preference.
     */
    public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml
     * to specify the icon background color. The value may or may not be used by Settings app.
     */
    public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_HINT =
            "com.android.settings.bg.hint";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml
     * to specify the icon background color as raw ARGB.
     */
    public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB =
            "com.android.settings.bg.argb";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
     * content provider providing the icon that should be displayed for the preference.
     *
     * <p>Icon provided by the content provider overrides any static icon.
     */
    public static final String META_DATA_PREFERENCE_ICON_URI = "com.android.settings.icon_uri";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml to specify whether
     * the icon is tintable. This should be a boolean value {@code true} or {@code false}, set using
     * {@code android:value}
     */
    public static final String META_DATA_PREFERENCE_ICON_TINTABLE =
            "com.android.settings.icon_tintable";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml
     * to specify the title that should be displayed for the preference.
     *
     * <p>Note: It is preferred to provide this value using {@code android:resource} with a string
     * resource for localization.
     */
    public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
     * content provider providing the title text that should be displayed for the preference.
     *
     * <p>Title provided by the content provider overrides any static title.
     */
    public static final String META_DATA_PREFERENCE_TITLE_URI = "com.android.settings.title_uri";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
     * summary text that should be displayed for the preference.
     */
    public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
     * content provider providing the summary text that should be displayed for the preference.
     *
     * <p>Summary provided by the content provider overrides any static summary.
     */
    public static final String META_DATA_PREFERENCE_SUMMARY_URI =
            "com.android.settings.summary_uri";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
     * content provider providing the switch that should be displayed for the preference.
     *
     * <p>This works with {@link #META_DATA_PREFERENCE_KEYHINT} which should also be set in the
     * AndroidManifest.xml
     */
    public static final String META_DATA_PREFERENCE_SWITCH_URI = "com.android.settings.switch_uri";

    /**
     * Name of the meta-data item that can be set from the content provider providing the intent
     * that will be executed when the user taps on the preference.
     */
    public static final String META_DATA_PREFERENCE_PENDING_INTENT =
            "com.android.settings.pending_intent";

    /**
     * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, the app will
     * always be run in the primary profile.
     *
     * @see #META_DATA_KEY_PROFILE
     */
    public static final String PROFILE_PRIMARY = "primary_profile_only";

    /**
     * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, the user
     * will be presented with a dialog to choose the profile the app will be run in.
     *
     * @see #META_DATA_KEY_PROFILE
     */
    public static final String PROFILE_ALL = "all_profiles";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
     * profile in which the app should be run when the device has a managed profile. The default
     * value is {@link #PROFILE_ALL} which means the user will be presented with a dialog to choose
     * the profile. If set to {@link #PROFILE_PRIMARY} the app will always be run in the primary
     * profile.
     *
     * @see #PROFILE_PRIMARY
     * @see #PROFILE_ALL
     */
    public static final String META_DATA_KEY_PROFILE = "com.android.settings.profile";

    /**
     * Name of the meta-data item that should be set in the AndroidManifest.xml to specify whether
     * the {@link android.app.Activity} should be launched in a separate task. This should be a
     * boolean value {@code true} or {@code false}, set using {@code android:value}
     */
    public static final String META_DATA_NEW_TASK = "com.android.settings.new_task";

    /** If the entry should be shown in settings search results. Defaults to true. */
    public static final String META_DATA_PREFERENCE_SEARCHABLE = "com.android.settings.searchable";

    /** Build a list of DashboardCategory. */
    public static List<DashboardCategory> getCategories(Context context,
            Map<Pair<String, String>, Tile> cache) {
        final long startTime = System.currentTimeMillis();
        final boolean setup =
                Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0;
        final ArrayList<Tile> tiles = new ArrayList<>();
        final UserManager userManager = (UserManager) context.getSystemService(
                Context.USER_SERVICE);
        for (UserHandle user : userManager.getUserProfiles()) {
            // TODO: Needs much optimization, too many PM queries going on here.
            if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
                // Only add Settings for this user.
                loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
                loadTilesForAction(context, user, OPERATOR_SETTINGS, cache,
                        OPERATOR_DEFAULT_CATEGORY, tiles, false);
                loadTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
                        MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
            }
            if (setup) {
                loadTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
                loadTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
            }
        }

        final HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
        for (Tile tile : tiles) {
            final String categoryKey = tile.getCategory();
            DashboardCategory category = categoryMap.get(categoryKey);
            if (category == null) {
                category = new DashboardCategory(categoryKey);

                if (category == null) {
                    Log.w(LOG_TAG, "Couldn't find category " + categoryKey);
                    continue;
                }
                categoryMap.put(categoryKey, category);
            }
            category.addTile(tile);
        }
        final ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
        for (DashboardCategory category : categories) {
            category.sortTiles();
        }

        if (DEBUG_TIMING) {
            Log.d(LOG_TAG, "getCategories took "
                    + (System.currentTimeMillis() - startTime) + " ms");
        }
        return categories;
    }

    @VisibleForTesting
    static void loadTilesForAction(Context context,
            UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
            String defaultCategory, List<Tile> outTiles, boolean requireSettings) {
        final Intent intent = new Intent(action);
        if (requireSettings) {
            intent.setPackage(SETTING_PKG);
        }
        loadActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent);
        loadProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent);
    }

    private static void loadActivityTiles(Context context,
            UserHandle user, Map<Pair<String, String>, Tile> addedCache,
            String defaultCategory, List<Tile> outTiles, Intent intent) {
        final PackageManager pm = context.getPackageManager();
        final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
                PackageManager.GET_META_DATA, user.getIdentifier());
        for (ResolveInfo resolved : results) {
            if (!resolved.system) {
                // Do not allow any app to add to settings, only system ones.
                continue;
            }
            final ActivityInfo activityInfo = resolved.activityInfo;
            final Bundle metaData = activityInfo.metaData;
            loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, activityInfo);
        }
    }

    private static void loadProviderTiles(Context context,
            UserHandle user, Map<Pair<String, String>, Tile> addedCache,
            String defaultCategory, List<Tile> outTiles, Intent intent) {
        final PackageManager pm = context.getPackageManager();
        final List<ResolveInfo> results =
                pm.queryIntentContentProvidersAsUser(intent, 0 /* flags */, user.getIdentifier());
        for (ResolveInfo resolved : results) {
            if (!resolved.system) {
                // Do not allow any app to add to settings, only system ones.
                continue;
            }
            final ProviderInfo providerInfo = resolved.providerInfo;
            final List<Bundle> entryData = getEntryDataFromProvider(
                    // Build new context so the entry data is retrieved for the queried user.
                    context.createContextAsUser(user, 0 /* flags */),
                    providerInfo.authority);
            if (entryData == null || entryData.isEmpty()) {
                continue;
            }
            for (Bundle metaData : entryData) {
                loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData,
                        providerInfo);
            }
        }
    }

    private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,
            String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,
            ComponentInfo componentInfo) {
        // Skip loading tile if the component is tagged primary_profile_only but not running on
        // the current user.
        if (user.getIdentifier() != ActivityManager.getCurrentUser()
                && Tile.isPrimaryProfileOnly(componentInfo.metaData)) {
            Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
                    + intent + " is primary profile only, skip loading tile for uid "
                    + user.getIdentifier());
            return;
        }

        String categoryKey = defaultCategory;
        // Load category
        if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY))
                && categoryKey == null) {
            Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
                    + intent + " missing metadata "
                    + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
            return;
        } else {
            categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
        }

        final boolean isProvider = componentInfo instanceof ProviderInfo;
        final Pair<String, String> key = isProvider
                ? new Pair<>(((ProviderInfo) componentInfo).authority,
                        metaData.getString(META_DATA_PREFERENCE_KEYHINT))
                : new Pair<>(componentInfo.packageName, componentInfo.name);
        Tile tile = addedCache.get(key);
        if (tile == null) {
            tile = isProvider
                    ? new ProviderTile((ProviderInfo) componentInfo, categoryKey, metaData)
                    : new ActivityTile((ActivityInfo) componentInfo, categoryKey);
            addedCache.put(key, tile);
        } else {
            tile.setMetaData(metaData);
        }

        tile.setGroupKey(metaData.getString(META_DATA_PREFERENCE_GROUP_KEY));

        if (!tile.userHandle.contains(user)) {
            tile.userHandle.add(user);
        }
        if (metaData.containsKey(META_DATA_PREFERENCE_PENDING_INTENT)) {
            tile.pendingIntentMap.put(
                    user, metaData.getParcelable(META_DATA_PREFERENCE_PENDING_INTENT));
        }
        if (!outTiles.contains(tile)) {
            outTiles.add(tile);
        }
    }

    /** Returns the entry data of the key specified from the provider */
    // TODO(b/144732809): rearrange methods by access level modifiers
    static Bundle getEntryDataFromProvider(Context context, String authority, String key) {
        final Map<String, IContentProvider> providerMap = new ArrayMap<>();
        final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA, key);
        Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */);
        if (result == null) {
            Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA, key);
            result = getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */);
        }
        return result;
    }

    /** Returns all entry data from the provider */
    private static List<Bundle> getEntryDataFromProvider(Context context, String authority) {
        final Map<String, IContentProvider> providerMap = new ArrayMap<>();
        final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA);
        final Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */);
        if (result != null) {
            return result.getParcelableArrayList(EntriesProvider.EXTRA_ENTRY_DATA);
        } else {
            Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA);
            Bundle fallbackResult =
                    getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */);
            return fallbackResult != null
                    ? fallbackResult.getParcelableArrayList(EntriesProvider.EXTRA_SWITCH_DATA)
                    : null;
        }
    }

    /**
     * Returns the complete uri from the meta data key of the tile.
     *
     * <p>A complete uri should contain at least one path segment and be one of the following types:
     * <br>content://authority/method
     * <br>content://authority/method/key
     *
     * <p>If the uri from the tile is not complete, build a uri by the default method and the
     * preference key.
     *
     * @param tile Tile which contains meta data
     * @param metaDataKey Key mapping to the uri in meta data
     * @param defaultMethod Method to be attached to the uri by default if it has no path segment
     * @return Uri associated with the key
     */
    public static Uri getCompleteUri(Tile tile, String metaDataKey, String defaultMethod) {
        final String uriString = tile.getMetaData().getString(metaDataKey);
        if (TextUtils.isEmpty(uriString)) {
            return null;
        }

        final Uri uri = Uri.parse(uriString);
        final List<String> pathSegments = uri.getPathSegments();
        if (pathSegments != null && !pathSegments.isEmpty()) {
            return uri;
        }

        final String key = tile.getMetaData().getString(META_DATA_PREFERENCE_KEYHINT);
        if (TextUtils.isEmpty(key)) {
            Log.w(LOG_TAG, "Please specify the meta-data " + META_DATA_PREFERENCE_KEYHINT
                    + " in AndroidManifest.xml for " + uriString);
            return buildUri(uri.getAuthority(), defaultMethod);
        }
        return buildUri(uri.getAuthority(), defaultMethod, key);
    }

    static Uri buildUri(String authority, String method, String key) {
        return new Uri.Builder()
                .scheme(ContentResolver.SCHEME_CONTENT)
                .authority(authority)
                .appendPath(method)
                .appendPath(key)
                .build();
    }

    private static Uri buildUri(String authority, String method) {
        return new Uri.Builder()
                .scheme(ContentResolver.SCHEME_CONTENT)
                .authority(authority)
                .appendPath(method)
                .build();
    }

    /**
     * Gets the icon package name and resource id from content provider.
     *
     * @param context context
     * @param packageName package name of the target activity
     * @param uri URI for the content provider
     * @param providerMap Maps URI authorities to providers
     * @return package name and resource id of the icon specified
     */
    public static Pair<String, Integer> getIconFromUri(Context context, String packageName,
            Uri uri, Map<String, IContentProvider> providerMap) {
        final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
        if (bundle == null) {
            return null;
        }
        final String iconPackageName = bundle.getString(EXTRA_PREFERENCE_ICON_PACKAGE);
        if (TextUtils.isEmpty(iconPackageName)) {
            return null;
        }
        int resId = bundle.getInt(META_DATA_PREFERENCE_ICON, 0);
        if (resId == 0) {
            return null;
        }
        // Icon can either come from the target package or from the Settings app.
        if (iconPackageName.equals(packageName)
                || iconPackageName.equals(context.getPackageName())) {
            return Pair.create(iconPackageName, resId);
        }
        return null;
    }

    /**
     * Gets text associated with the input key from the content provider.
     *
     * @param context context
     * @param uri URI for the content provider
     * @param providerMap Maps URI authorities to providers
     * @param key Key mapping to the text in bundle returned by the content provider
     * @return Text associated with the key, if returned by the content provider
     */
    public static String getTextFromUri(Context context, Uri uri,
            Map<String, IContentProvider> providerMap, String key) {
        final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
        return (bundle != null) ? bundle.getString(key) : null;
    }

    /**
     * Gets boolean associated with the input key from the content provider.
     *
     * @param context context
     * @param uri URI for the content provider
     * @param providerMap Maps URI authorities to providers
     * @param key Key mapping to the text in bundle returned by the content provider
     * @return Boolean associated with the key, if returned by the content provider
     */
    public static boolean getBooleanFromUri(Context context, Uri uri,
            Map<String, IContentProvider> providerMap, String key) {
        final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
        return (bundle != null) ? bundle.getBoolean(key) : false;
    }

    /**
     * Puts boolean associated with the input key to the content provider.
     *
     * @param context context
     * @param uri URI for the content provider
     * @param providerMap Maps URI authorities to providers
     * @param key Key mapping to the text in bundle returned by the content provider
     * @param value Boolean associated with the key
     * @return Bundle associated with the action, if returned by the content provider
     */
    public static Bundle putBooleanToUriAndGetResult(Context context, Uri uri,
            Map<String, IContentProvider> providerMap, String key, boolean value) {
        final Bundle bundle = new Bundle();
        bundle.putBoolean(key, value);
        return getBundleFromUri(context, uri, providerMap, bundle);
    }

    private static Bundle getBundleFromUri(Context context, Uri uri,
            Map<String, IContentProvider> providerMap, Bundle bundle) {
        final Pair<String, String> args = getMethodAndKey(uri);
        if (args == null) {
            return null;
        }
        final String method = args.first;
        final String key = args.second;
        if (TextUtils.isEmpty(method)) {
            return null;
        }
        final IContentProvider provider = getProviderFromUri(context, uri, providerMap);
        if (provider == null) {
            return null;
        }
        if (!TextUtils.isEmpty(key)) {
            if (bundle == null) {
                bundle = new Bundle();
            }
            bundle.putString(META_DATA_PREFERENCE_KEYHINT, key);
        }
        try {
            return provider.call(context.getAttributionSource(),
                    uri.getAuthority(), method, uri.toString(), bundle);
        } catch (RemoteException e) {
            return null;
        }
    }

    private static IContentProvider getProviderFromUri(Context context, Uri uri,
            Map<String, IContentProvider> providerMap) {
        if (uri == null) {
            return null;
        }
        final String authority = uri.getAuthority();
        if (TextUtils.isEmpty(authority)) {
            return null;
        }
        if (!providerMap.containsKey(authority)) {
            providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
        }
        return providerMap.get(authority);
    }

    /** Returns method and key of the complete uri. */
    private static Pair<String, String> getMethodAndKey(Uri uri) {
        if (uri == null) {
            return null;
        }
        final List<String> pathSegments = uri.getPathSegments();
        if (pathSegments == null || pathSegments.isEmpty()) {
            return null;
        }
        final String method = pathSegments.get(0);
        final String key = pathSegments.size() > 1 ? pathSegments.get(1) : null;
        return Pair.create(method, key);
    }
}
