blob: 58c2707a1f1966ad45c90a951623c6e48f99e74c [file] [log] [blame]
/*
* 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;
}
}
}