// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base;

import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.TransactionTooLargeException;

import java.util.Collections;
import java.util.List;

/**
 * This class provides Android PackageManager related utility methods.
 */
public class PackageManagerUtils {
    private static final String TAG = "PackageManagerUtils";

    // This is the intent Android uses internally to detect browser apps.
    // See
    // https://cs.android.com/android/_/android/platform/packages/modules/Permission/+/android12-release:PermissionController/src/com/android/permissioncontroller/role/model/BrowserRoleBehavior.java;drc=86fa7d5dfa43f66b170f93ade4f59b9a770be32f;l=50
    public static final Intent BROWSER_INTENT = new Intent()
                                                        .setAction(Intent.ACTION_VIEW)
                                                        .addCategory(Intent.CATEGORY_BROWSABLE)
                                                        .setData(Uri.fromParts("http", "", null));

    /**
     * Retrieve information about the Activity that will handle the given Intent.
     *
     * Note: This function is expensive on KK and below and should not be called from main thread
     * when avoidable.
     *
     * @param intent Intent to resolve.
     * @param flags The PackageManager flags to pass to resolveActivity().
     * @return       ResolveInfo of the Activity that will handle the Intent, or null if it failed.
     */
    public static ResolveInfo resolveActivity(Intent intent, int flags) {
        // On KitKat, calling PackageManager#resolveActivity() causes disk reads and
        // writes. Temporarily allow this while resolving the intent.
        try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
            PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
            return pm.resolveActivity(intent, flags);
        } catch (RuntimeException e) {
            handleExpectedExceptionsOrRethrow(e, intent);
        }
        return null;
    }

    /**
     * Get the list of component name of activities which can resolve |intent|.  If the request
     * fails, an empty list will be returned.
     *
     * See {@link PackageManager#queryIntentActivities(Intent, int)}
     */
    public static List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
        // Allowlist for Samsung. See http://crbug.com/613977 and https://crbug.com/894160 for more
        // context.
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
            return pm.queryIntentActivities(intent, flags);
        } catch (RuntimeException e) {
            handleExpectedExceptionsOrRethrow(e, intent);
        }
        return Collections.emptyList();
    }

    /**
     * Check if the given Intent can be resolved by any Activities on the system.
     *
     * See {@link PackageManagerUtils#queryIntentActivities(Intent, int)}
     */
    public static boolean canResolveActivity(Intent intent, int flags) {
        return !queryIntentActivities(intent, flags).isEmpty();
    }

    /**
     * Check if the given Intent can be resolved by any Activities on the system.
     *
     * See {@link PackageManagerUtils#canResolveActivity(Intent, int)}
     */
    public static boolean canResolveActivity(Intent intent) {
        return canResolveActivity(intent, 0);
    }

    /**
     * @return Intent to query a list of installed home launchers.
     */
    public static Intent getQueryInstalledHomeLaunchersIntent() {
        return new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
    }

    /**
     * @return Default ResolveInfo to handle a VIEW intent for a url.
     */
    public static ResolveInfo resolveDefaultWebBrowserActivity() {
        return resolveActivity(BROWSER_INTENT, PackageManager.MATCH_DEFAULT_ONLY);
    }

    /**
     * @return The list of names of web browser applications available in the system. A browser
     *         may appear twice if it has multiple intent handlers.
     */
    public static List<ResolveInfo> queryAllWebBrowsersInfo() {
        // Copying these flags from Android source for detecting the list of installed browsers.
        // Apparently MATCH_ALL doesn't include MATCH_DIRECT_BOOT_*.
        // See
        // https://cs.android.com/android/_/android/platform/packages/modules/Permission/+/android12-release:PermissionController/src/com/android/permissioncontroller/role/model/BrowserRoleBehavior.java;drc=86fa7d5dfa43f66b170f93ade4f59b9a770be32f;l=114
        int flags = PackageManager.MATCH_ALL | PackageManager.MATCH_DIRECT_BOOT_AWARE
                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DEFAULT_ONLY;
        return queryIntentActivities(BROWSER_INTENT, flags);
    }

    /**
     * @return The list of names of system launcher applications available in the system.
     */
    public static List<ResolveInfo> queryAllLaunchersInfo() {
        return queryIntentActivities(
                getQueryInstalledHomeLaunchersIntent(), PackageManager.MATCH_ALL);
    }

    // See https://crbug.com/700505 and https://crbug.com/369574.
    private static void handleExpectedExceptionsOrRethrow(RuntimeException e, Intent intent) {
        if (e instanceof NullPointerException
                || e.getCause() instanceof TransactionTooLargeException) {
            Log.e(TAG, "Could not resolve Activity for intent " + intent.toString(), e);
        } else {
            throw e;
        }
    }
}
