/*
 * 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 android.annotation.IntDef;
import android.annotation.NonNull;
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.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.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 = ",";

    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.
     *
     * @param context context
     * @param userId userId
     * @param pendingIntent pending intent
     * @return Can be {@code null} if pending intent was null.
     */
    public static MediaButtonReceiverHolder create(Context context, int userId,
            PendingIntent pendingIntent) {
        if (pendingIntent == null) {
            return null;
        }
        ComponentName componentName = (pendingIntent != null && pendingIntent.getIntent() != null)
                ? pendingIntent.getIntent().getComponent() : null;
        if (componentName != null) {
            // Explicit intent, where component name is in the PendingIntent.
            return new MediaButtonReceiverHolder(userId, pendingIntent, componentName,
                    getComponentType(context, componentName));
        }

        // Implicit intent, where component name isn't in the PendingIntent. Try resolve.
        PackageManager pm = context.getPackageManager();
        Intent intent = pendingIntent.getIntent();
        if ((componentName = resolveImplicitServiceIntent(pm, intent)) != null) {
            return new MediaButtonReceiverHolder(
                    userId, pendingIntent, componentName, COMPONENT_TYPE_SERVICE);
        } else if ((componentName = resolveManifestDeclaredBroadcastReceiverIntent(pm, intent))
                != null) {
            return new MediaButtonReceiverHolder(
                    userId, pendingIntent, componentName, COMPONENT_TYPE_BROADCAST);
        } else if ((componentName = resolveImplicitActivityIntent(pm, intent)) != null) {
            return new MediaButtonReceiverHolder(
                    userId, pendingIntent, componentName, COMPONENT_TYPE_ACTIVITY);
        }

        // Failed to resolve target component for the pending intent. It's unlikely to be usable.
        // However, the pending intent would be still used, just to follow the legacy behavior.
        Log.w(TAG, "Unresolvable implicit intent is set, pi=" + pendingIntent);
        String packageName = (pendingIntent != null && pendingIntent.getIntent() != null)
                ? pendingIntent.getIntent().getPackage() : null;
        return new MediaButtonReceiverHolder(userId, pendingIntent,
                packageName != null ? packageName : "");
    }

    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 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.
     * @see PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler)
     */
    public boolean send(Context context, KeyEvent keyEvent, String callingPackageName,
            int resultCode, PendingIntent.OnFinished onFinishedListener, Handler handler) {
        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, callingPackageName);

        if (mPendingIntent != null) {
            if (DEBUG_KEY_EVENT) {
                Log.d(TAG, "Sending " + keyEvent + " to the last known PendingIntent "
                        + mPendingIntent);
            }
            try {
                mPendingIntent.send(
                        context, resultCode, mediaButtonIntent, onFinishedListener, handler);
            } 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.startForegroundServiceAsUser(mediaButtonIntent,
                                userHandle);
                        break;
                    default:
                        // Legacy behavior for other cases.
                        context.sendBroadcastAsUser(mediaButtonIntent, userHandle);
                }
            } 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() {
        if (mPendingIntent != null) {
            return "MBR {pi=" + mPendingIntent + ", type=" + mComponentType + "}";
        }
        return "Restored MBR {component=" + mComponentName + ", type=" + mComponentType + "}";
    }

    /**
     * @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.toString(),
                String.valueOf(mUserId),
                String.valueOf(mComponentType));
    }

    /**
     * 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,
                    PackageManager.MATCH_DIRECT_BOOT_AWARE
                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                            | PackageManager.GET_ACTIVITIES);
            if (activityInfo != null) {
                return COMPONENT_TYPE_ACTIVITY;
            }
        } catch (PackageManager.NameNotFoundException e) {
        }
        try {
            ServiceInfo serviceInfo = pm.getServiceInfo(componentName,
                    PackageManager.MATCH_DIRECT_BOOT_AWARE
                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                            | 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 resolveImplicitServiceIntent(PackageManager pm, Intent intent) {
        // Flag explanations.
        // - MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE:
        //     filter apps regardless of the phone's locked/unlocked state.
        // - GET_SERVICES: Return service
        return createComponentName(pm.resolveService(intent,
                PackageManager.MATCH_DIRECT_BOOT_AWARE
                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                        | PackageManager.GET_SERVICES));
    }

    private static ComponentName resolveManifestDeclaredBroadcastReceiverIntent(
            PackageManager pm, Intent intent) {
        // Flag explanations.
        // - MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE:
        //     filter apps regardless of the phone's locked/unlocked state.
        List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(intent,
                PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
        return (resolveInfos != null && !resolveInfos.isEmpty())
                ? createComponentName(resolveInfos.get(0)) : null;
    }

    private static ComponentName resolveImplicitActivityIntent(PackageManager pm, Intent intent) {
        // Flag explanations.
        // - MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE:
        //     Filter apps regardless of the phone's locked/unlocked state.
        // - MATCH_DEFAULT_ONLY:
        //     Implicit intent receiver should be set as default. Only needed for activity.
        // - GET_ACTIVITIES: Return activity
        return createComponentName(pm.resolveActivity(intent,
                PackageManager.MATCH_DIRECT_BOOT_AWARE
                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                        | PackageManager.MATCH_DEFAULT_ONLY
                        | PackageManager.GET_ACTIVITIES));
    }

    private static ComponentName createComponentName(ResolveInfo resolveInfo) {
        if (resolveInfo == null) {
            return null;
        }
        ComponentInfo componentInfo;
        // Code borrowed from ResolveInfo#getComponentInfo().
        if (resolveInfo.activityInfo != null) {
            componentInfo = resolveInfo.activityInfo;
        } else if (resolveInfo.serviceInfo != null) {
            componentInfo = resolveInfo.serviceInfo;
        } else {
            // We're not interested in content provider.
            return null;
        }
        // Code borrowed from ComponentInfo#getComponentName().
        try {
            return new ComponentName(componentInfo.packageName, componentInfo.name);
        } catch (IllegalArgumentException | NullPointerException e) {
            // This may be happen if resolveActivity() end up with matching multiple activities.
            // see PackageManager#resolveActivity().
            return null;
        }
    }
}
