Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit dab21fdb authored by Jaewan Kim's avatar Jaewan Kim
Browse files

MediaSessionService: Fix crash with implicit media button receiver

PendingIntent can be built with implicit intent (i.e. intent without
ComponentName), while using it cause exception.
However, apps can still create and set media button receiver with
implicit intent. Prevent crash when MediaSessionService is notify about
such app's media button receiver.

Bug: 147536687
Test: Build and run
Change-Id: Icfdd45edaa8e25dbdeca3c2180bd234718eef202
parent 45b0fc87
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -266,8 +266,12 @@ public final class MediaSession {
     * playback after the session has been stopped. If your app is started in
     * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via
     * the pending intent.
     * <p>
     * The pending intent is recommended to be explicit to follow the security recommendation of
     * {@link PendingIntent#getActivity}.
     *
     * @param mbr The {@link PendingIntent} to send the media button event to.
     * @see PendingIntent#getActivity
     */
    public void setMediaButtonReceiver(@Nullable PendingIntent mbr) {
        try {
+360 −0
Original line number Diff line number Diff line
/*
 * 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]);
        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;
        }
    }
}
+10 −9
Original line number Diff line number Diff line
@@ -127,7 +127,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR
            new ArrayList<>();

    private long mFlags;
    private PendingIntent mMediaButtonReceiver;
    private MediaButtonReceiverHolder mMediaButtonReceiverHolder;
    private PendingIntent mLaunchIntent;

    // TransportPerformer fields
@@ -220,8 +220,8 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR
     *
     * @return The pending intent set by the app or null.
     */
    public PendingIntent getMediaButtonReceiver() {
        return mMediaButtonReceiver;
    public MediaButtonReceiverHolder getMediaButtonReceiver() {
        return mMediaButtonReceiverHolder;
    }

    /**
@@ -471,7 +471,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR
                + ", userId=" + mUserId);
        pw.println(indent + "package=" + mPackageName);
        pw.println(indent + "launchIntent=" + mLaunchIntent);
        pw.println(indent + "mediaButtonReceiver=" + mMediaButtonReceiver);
        pw.println(indent + "mediaButtonReceiver=" + mMediaButtonReceiverHolder);
        pw.println(indent + "active=" + mIsActive);
        pw.println(indent + "flags=" + mFlags);
        pw.println(indent + "rating type=" + mRatingType);
@@ -833,12 +833,14 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR

        @Override
        public void setMediaButtonReceiver(PendingIntent pi) throws RemoteException {
            if ((mPolicies & SessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER) == 1) {
                return;
            }
            mMediaButtonReceiver = pi;
            final long token = Binder.clearCallingIdentity();
            try {
                if ((mPolicies & SessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER)
                        != 0) {
                    return;
                }
                mMediaButtonReceiverHolder =
                        MediaButtonReceiverHolder.create(mContext, mUserId, pi);
                mService.onMediaButtonReceiverChanged(MediaSessionRecord.this);
            } finally {
                Binder.restoreCallingIdentity(token);
@@ -1529,5 +1531,4 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR
            msg.sendToTarget();
        }
    }

}
+32 −157

File changed.

Preview size limit exceeded, changes collapsed.