blob: c5fb5270f84178d59ab979b778487f4f9b8c7b33 [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.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 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 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, String callingPackageName,
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, callingPackageName);
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setTemporaryAppAllowlist(fgsAllowlistDurationMs,
PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
PowerWhitelistManager.REASON_MEDIA_BUTTON, "");
options.setBackgroundActivityStartsAllowed(true);
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,
/* 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.
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() {
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.flattenToString(),
String.valueOf(mUserId),
String.valueOf(mComponentType));
}
@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) {
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;
}
}
}