/*
 * Copyright 2020 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.server.media;

import static com.android.media.mediasession.flags.Flags.addWiuAllowlistingToMediaButtonReceiverHolderSend;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.BroadcastOptions;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.os.PowerWhitelistManager;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;

/**
 * Holds the media button receiver, and also provides helper methods around it.
 */
final class MediaButtonReceiverHolder {
    public static final int COMPONENT_TYPE_INVALID = 0;
    public static final int COMPONENT_TYPE_BROADCAST = 1;
    public static final int COMPONENT_TYPE_ACTIVITY = 2;
    public static final int COMPONENT_TYPE_SERVICE = 3;

    @IntDef(value = {
            COMPONENT_TYPE_INVALID,
            COMPONENT_TYPE_BROADCAST,
            COMPONENT_TYPE_ACTIVITY,
            COMPONENT_TYPE_SERVICE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ComponentType {}

    private static final String TAG = "PendingIntentHolder";
    private static final boolean DEBUG_KEY_EVENT = MediaSessionService.DEBUG_KEY_EVENT;
    private static final String COMPONENT_NAME_USER_ID_DELIM = ",";
    // Filter apps regardless of the phone's locked/unlocked state.
    private static final int PACKAGE_MANAGER_COMMON_FLAGS =
            PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
    private static final int MAX_COMPONENT_NAME_LENGTH = 1024;
    private final int mUserId;
    private final PendingIntent mPendingIntent;
    private final ComponentName mComponentName;
    private final String mPackageName;
    @ComponentType
    private final int mComponentType;

    /**
     * Unflatten from string which is previously flattened string via flattenToString().
     * <p>
     * It's used to store and restore media button receiver across the boot, by keeping the intent's
     * component name to the persistent storage.
     *
     * @param mediaButtonReceiverInfo previously flattened string via flattenToString()
     * @return new instance if the string was valid. {@code null} otherwise.
     */
    public static MediaButtonReceiverHolder unflattenFromString(
            Context context, String mediaButtonReceiverInfo) {
        if (TextUtils.isEmpty(mediaButtonReceiverInfo)) {
            return null;
        }
        String[] tokens = mediaButtonReceiverInfo.split(COMPONENT_NAME_USER_ID_DELIM);
        if (tokens == null || (tokens.length != 2 && tokens.length != 3)) {
            return null;
        }
        ComponentName componentName = ComponentName.unflattenFromString(tokens[0]);
        if (componentName == null) {
            return null;
        }
        int userId = Integer.parseInt(tokens[1]);
        // Guess component type if the OS version is updated from the older version.
        int componentType = (tokens.length == 3)
                ?  Integer.parseInt(tokens[2])
                : getComponentType(context, componentName);
        return new MediaButtonReceiverHolder(userId, null, componentName, componentType);
    }

    /**
     * Creates a new instance from a {@link PendingIntent}.
     *
     * <p>This method assumes the session package name has been validated and effectively belongs to
     * the media session's owner.
     *
     * @param userId userId
     * @param pendingIntent pending intent that will receive media button events
     * @param sessionPackageName package name of media session owner
     * @return {@link MediaButtonReceiverHolder} instance or {@code null} if pending intent was
     *     null.
     */
    public static MediaButtonReceiverHolder create(
            int userId, @Nullable PendingIntent pendingIntent, String sessionPackageName) {
        if (pendingIntent == null) {
            return null;
        }
        int componentType = getComponentType(pendingIntent);
        ComponentName componentName = getComponentName(pendingIntent, componentType);
        if (componentName != null) {
            return new MediaButtonReceiverHolder(userId, pendingIntent, componentName,
                    componentType);
        }

        // Failed to resolve target component for the pending intent. It's unlikely to be usable.
        // However, the pending intent would be still used, so setting the package name to the
        // package name of the session that set this pending intent.
        Log.w(TAG, "Unresolvable implicit intent is set, pi=" + pendingIntent);
        return new MediaButtonReceiverHolder(userId, pendingIntent, sessionPackageName);
    }

    public static MediaButtonReceiverHolder create(int userId, ComponentName broadcastReceiver) {
        return new MediaButtonReceiverHolder(userId, null, broadcastReceiver,
                COMPONENT_TYPE_BROADCAST);
    }

    private MediaButtonReceiverHolder(int userId, PendingIntent pendingIntent,
            ComponentName componentName, @ComponentType int componentType) {
        mUserId = userId;
        mPendingIntent = pendingIntent;
        mComponentName = componentName;
        mPackageName = componentName.getPackageName();
        mComponentType = componentType;
    }

    private MediaButtonReceiverHolder(int userId, PendingIntent pendingIntent, String packageName) {
        mUserId = userId;
        mPendingIntent = pendingIntent;
        mComponentName = null;
        mPackageName = packageName;
        mComponentType = COMPONENT_TYPE_INVALID;
    }

    /**
     * @return the user id
     */
    public int getUserId() {
        return mUserId;
    }

    /**
     * @return package name that the media button receiver would be sent to.
     */
    @NonNull
    public String getPackageName() {
        return mPackageName;
    }

    /**
     * Sends the media key event to the media button receiver.
     *
     * <p>This prioritizes using use pending intent for sending media key event.
     *
     * @param context context to be used to call PendingIntent#send
     * @param keyEvent keyEvent to send
     * @param resultCode result code to be used to call PendingIntent#send. Ignored if there's no
     *     valid pending intent.
     * @param mediaSessionService {@link MediaSessionService} triggering the event
     * @param callingPackageName package name of the caller
     * @param callingPid PID of the caller
     * @param callingUid UID of the caller
     * @param reportedPackageName package name that is reported to the receiver using {@link
     *     Intent#EXTRA_PACKAGE_NAME}
     * @param onFinishedListener callback to be used to get result of PendingIntent#send. Ignored if
     *     there's no valid pending intent.
     * @param handler handler to be used to call onFinishedListener. Ignored if there's no valid
     *     pending intent.
     * @param fgsAllowlistDurationMs duration for which the media button receiver will be allowed to
     *     start FGS from BG.
     * @see PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler)
     */
    public boolean send(
            Context context,
            KeyEvent keyEvent,
            MediaSessionService mediaSessionService,
            String callingPackageName,
            int callingPid,
            int callingUid,
            String reportedPackageName,
            int resultCode,
            PendingIntent.OnFinished onFinishedListener,
            Handler handler,
            long fgsAllowlistDurationMs) {
        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        mediaButtonIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
        // TODO: Find a way to also send PID/UID in secure way.
        mediaButtonIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, reportedPackageName);

        // TODO: b/385736540 - Remove options and timeout constants for fgsAllowlistDurationMs once
        //  flag add_wiu_allowlisting_to_media_button_receiver_holder_send is rolled out.
        final BroadcastOptions options = BroadcastOptions.makeBasic();
        options.setTemporaryAppAllowlist(fgsAllowlistDurationMs,
                PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                PowerWhitelistManager.REASON_MEDIA_BUTTON, "");
        options.setBackgroundActivityStartsAllowed(true);
        if (addWiuAllowlistingToMediaButtonReceiverHolderSend()) {
            PackageManager packageManager = context.getPackageManager();
            try {
                int targetUid = packageManager.getPackageUidAsUser(mPackageName, mUserId);
                mediaSessionService.tempAllowlistTargetPkgIfPossible(
                        targetUid,
                        /* targetPackage= */ mPackageName,
                        callingPid,
                        callingUid,
                        callingPackageName,
                        /* reason= */ TAG);
            } catch (PackageManager.NameNotFoundException e) {
                // Package name doesn't exist.
                if (DEBUG_KEY_EVENT) {
                    Log.d(TAG, "Can't allowlist, package " + mPackageName + "doesn't exist");
                }
            }
        }
        if (mPendingIntent != null) {
            if (DEBUG_KEY_EVENT) {
                Log.d(TAG, "Sending " + keyEvent + " to the last known PendingIntent "
                        + mPendingIntent);
            }
            try {
                if (addWiuAllowlistingToMediaButtonReceiverHolderSend()) {
                    mPendingIntent.send(
                            context, resultCode, mediaButtonIntent, onFinishedListener, handler);
                } else {
                    mPendingIntent.send(
                            context,
                            resultCode,
                            mediaButtonIntent,
                            onFinishedListener,
                            handler,
                            /* requiredPermission= */ null,
                            options.toBundle());
                }
            } catch (PendingIntent.CanceledException e) {
                Log.w(TAG, "Error sending key event to media button receiver " + mPendingIntent, e);
                return false;
            }
        } else if (mComponentName != null) {
            if (DEBUG_KEY_EVENT) {
                Log.d(TAG, "Sending " + keyEvent + " to the restored intent "
                        + mComponentName + ", type=" + mComponentType);
            }
            mediaButtonIntent.setComponent(mComponentName);
            UserHandle userHandle = UserHandle.of(mUserId);
            try {
                switch (mComponentType) {
                    case COMPONENT_TYPE_ACTIVITY:
                        context.startActivityAsUser(mediaButtonIntent, userHandle);
                        break;
                    case COMPONENT_TYPE_SERVICE:
                        context.createContextAsUser(userHandle, 0).startForegroundService(
                                mediaButtonIntent);
                        break;
                    default:
                        // Legacy behavior for other cases.
                        if (addWiuAllowlistingToMediaButtonReceiverHolderSend()) {
                            context.sendBroadcastAsUser(mediaButtonIntent, userHandle);
                        } else {
                            context.sendBroadcastAsUser(
                                    mediaButtonIntent,
                                    userHandle,
                                    /* receiverPermission= */ null,
                                    options.toBundle());
                        }
                }
            } catch (Exception e) {
                Log.w(TAG, "Error sending media button to the restored intent "
                        + mComponentName + ", type=" + mComponentType, e);
                return false;
            }
        } else {
            // Leave log, just in case.
            Log.e(TAG, "Shouldn't be happen -- pending intent or component name must be set");
            return false;
        }
        return true;
    }


    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("MBR {pi=").append(mPendingIntent);
        sb.append(", componentName=").append(mComponentName);
        sb.append(", type=").append(mComponentType);
        sb.append(", pkg=").append(mPackageName);
        sb.append("}");
        return sb.toString();
    }

    /**
     * @return flattened string. Can be empty string if the MBR is created with implicit intent.
     */
    public String flattenToString() {
        if (mComponentName == null) {
            // We don't know which component would receive the key event.
            return "";
        }
        return String.join(COMPONENT_NAME_USER_ID_DELIM,
                mComponentName.flattenToString(),
                String.valueOf(mUserId),
                String.valueOf(mComponentType));
    }

    public ComponentName getComponentName() {
        return mComponentName;
    }

    @ComponentType
    private static int getComponentType(PendingIntent pendingIntent) {
        if (pendingIntent.isBroadcast()) {
            return COMPONENT_TYPE_BROADCAST;
        } else if (pendingIntent.isActivity()) {
            return COMPONENT_TYPE_ACTIVITY;
        } else if (pendingIntent.isForegroundService() || pendingIntent.isService()) {
            return COMPONENT_TYPE_SERVICE;
        }
        return COMPONENT_TYPE_INVALID;
    }

    /**
     * Gets the type of the component
     *
     * @param context context
     * @param componentName component name
     * @return A component type
     */
    @ComponentType
    private static int getComponentType(Context context, ComponentName componentName) {
        if (componentName == null) {
            return COMPONENT_TYPE_INVALID;
        }
        PackageManager pm = context.getPackageManager();
        try {
            ActivityInfo activityInfo = pm.getActivityInfo(componentName,
                    PACKAGE_MANAGER_COMMON_FLAGS | PackageManager.GET_ACTIVITIES);
            if (activityInfo != null) {
                return COMPONENT_TYPE_ACTIVITY;
            }
        } catch (PackageManager.NameNotFoundException e) {
        }
        try {
            ServiceInfo serviceInfo = pm.getServiceInfo(componentName,
                    PACKAGE_MANAGER_COMMON_FLAGS | PackageManager.GET_SERVICES);
            if (serviceInfo != null) {
                return COMPONENT_TYPE_SERVICE;
            }
        } catch (PackageManager.NameNotFoundException e) {
        }
        // Pick legacy behavior for BroadcastReceiver or unknown.
        return COMPONENT_TYPE_BROADCAST;
    }

    private static ComponentName getComponentName(PendingIntent pendingIntent, int componentType) {
        List<ResolveInfo> resolveInfos = Collections.emptyList();
        switch (componentType) {
            case COMPONENT_TYPE_ACTIVITY:
                resolveInfos = pendingIntent.queryIntentComponents(
                        PACKAGE_MANAGER_COMMON_FLAGS
                                | PackageManager.MATCH_DEFAULT_ONLY /* Implicit intent receiver
                                should be set as default. Only needed for activity. */
                                | PackageManager.GET_ACTIVITIES);
                break;
            case COMPONENT_TYPE_SERVICE:
                resolveInfos = pendingIntent.queryIntentComponents(
                        PACKAGE_MANAGER_COMMON_FLAGS | PackageManager.GET_SERVICES);
                break;
            case COMPONENT_TYPE_BROADCAST:
                resolveInfos = pendingIntent.queryIntentComponents(
                        PACKAGE_MANAGER_COMMON_FLAGS | PackageManager.GET_RECEIVERS);
                break;
        }

        for (ResolveInfo resolveInfo : resolveInfos) {
            ComponentInfo componentInfo = getComponentInfo(resolveInfo);
            if (componentInfo != null && TextUtils.equals(componentInfo.packageName,
                    pendingIntent.getCreatorPackage())
                    && componentInfo.packageName != null && componentInfo.name != null) {
                int componentNameLength =
                        componentInfo.packageName.length() + componentInfo.name.length() + 1;
                if (componentNameLength > MAX_COMPONENT_NAME_LENGTH) {
                    Log.w(TAG, "detected and ignored component name with overly long package"
                            + " or name, pi=" + pendingIntent);
                    continue;
                }
                return new ComponentName(componentInfo.packageName, componentInfo.name);
            }
        }

        return null;
    }

    /**
     * Retrieves the {@link ComponentInfo} from a {@link ResolveInfo} instance. Similar to {@link
     * ResolveInfo#getComponentInfo()}, but returns {@code null} if this {@link ResolveInfo} points
     * to a content provider.
     *
     * @param resolveInfo Where to extract the {@link ComponentInfo} from.
     * @return Either a non-null {@link ResolveInfo#activityInfo} or {@link
     *     ResolveInfo#serviceInfo}. Otherwise {@code null} if {@link ResolveInfo#providerInfo} is
     *     not {@code null}.
     */
    private static ComponentInfo getComponentInfo(@NonNull ResolveInfo resolveInfo) {
        // Code borrowed from ResolveInfo#getComponentInfo().
        if (resolveInfo.activityInfo != null) {
            return resolveInfo.activityInfo;
        } else if (resolveInfo.serviceInfo != null) {
            return resolveInfo.serviceInfo;
        } else {
            // We're not interested in content providers.
            return null;
        }
    }
}
