Allow privileged app to set media key event listener
If the media key listener is set, the listener will receive the media key events before any other sessions, but after the global priority session. If the event is handled by the listener, other sessions cannot get the event. Privileged app needs permission android.permission.SET_MEDIA_KEY_LISTENER to set the listener. Bug: 30125811 Change-Id: I2b2cf4ac7873b70899194701c6921990dcb9de02
This commit is contained in:
@@ -423,6 +423,7 @@ LOCAL_SRC_FILES += \
|
||||
media/java/android/media/projection/IMediaProjectionManager.aidl \
|
||||
media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl \
|
||||
media/java/android/media/session/IActiveSessionsListener.aidl \
|
||||
media/java/android/media/session/IOnMediaKeyListener.aidl \
|
||||
media/java/android/media/session/IOnVolumeKeyLongPressListener.aidl \
|
||||
media/java/android/media/session/ISession.aidl \
|
||||
media/java/android/media/session/ISessionCallback.aidl \
|
||||
|
||||
@@ -215,6 +215,7 @@ package android {
|
||||
field public static final java.lang.String SET_ALWAYS_FINISH = "android.permission.SET_ALWAYS_FINISH";
|
||||
field public static final java.lang.String SET_ANIMATION_SCALE = "android.permission.SET_ANIMATION_SCALE";
|
||||
field public static final java.lang.String SET_DEBUG_APP = "android.permission.SET_DEBUG_APP";
|
||||
field public static final java.lang.String SET_MEDIA_KEY_LISTENER = "android.permission.SET_MEDIA_KEY_LISTENER";
|
||||
field public static final java.lang.String SET_ORIENTATION = "android.permission.SET_ORIENTATION";
|
||||
field public static final java.lang.String SET_POINTER_SPEED = "android.permission.SET_POINTER_SPEED";
|
||||
field public static final deprecated java.lang.String SET_PREFERRED_APPLICATIONS = "android.permission.SET_PREFERRED_APPLICATIONS";
|
||||
@@ -24820,6 +24821,7 @@ package android.media.session {
|
||||
method public void addOnActiveSessionsChangedListener(android.media.session.MediaSessionManager.OnActiveSessionsChangedListener, android.content.ComponentName, android.os.Handler);
|
||||
method public java.util.List<android.media.session.MediaController> getActiveSessions(android.content.ComponentName);
|
||||
method public void removeOnActiveSessionsChangedListener(android.media.session.MediaSessionManager.OnActiveSessionsChangedListener);
|
||||
method public void setOnMediaKeyListener(android.media.session.MediaSessionManager.OnMediaKeyListener, android.os.Handler);
|
||||
method public void setOnVolumeKeyLongPressListener(android.media.session.MediaSessionManager.OnVolumeKeyLongPressListener, android.os.Handler);
|
||||
}
|
||||
|
||||
@@ -24827,6 +24829,10 @@ package android.media.session {
|
||||
method public abstract void onActiveSessionsChanged(java.util.List<android.media.session.MediaController>);
|
||||
}
|
||||
|
||||
public static abstract interface MediaSessionManager.OnMediaKeyListener {
|
||||
method public abstract boolean onMediaKey(android.view.KeyEvent);
|
||||
}
|
||||
|
||||
public static abstract interface MediaSessionManager.OnVolumeKeyLongPressListener {
|
||||
method public abstract void onVolumeKeyLongPress(android.view.KeyEvent);
|
||||
}
|
||||
|
||||
@@ -2632,6 +2632,14 @@
|
||||
<permission android:name="android.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER"
|
||||
android:protectionLevel="signature|privileged|development" />
|
||||
|
||||
<!-- @SystemApi @hide Allows an application to set media key event listener.
|
||||
<p>When it's set, the application will receive the media key event before
|
||||
any other media sessions. If the event is handled by the listener, other sessions
|
||||
cannot get the event.</p>
|
||||
<p>Not for use by third-party applications</p> -->
|
||||
<permission android:name="android.permission.SET_MEDIA_KEY_LISTENER"
|
||||
android:protectionLevel="signature|privileged|development" />
|
||||
|
||||
<!-- @SystemApi Required to be able to disable the device (very dangerous!).
|
||||
<p>Not for use by third-party applications.
|
||||
@hide
|
||||
|
||||
28
media/java/android/media/session/IOnMediaKeyListener.aidl
Normal file
28
media/java/android/media/session/IOnMediaKeyListener.aidl
Normal file
@@ -0,0 +1,28 @@
|
||||
/* Copyright (C) 2016 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 android.media.session;
|
||||
|
||||
import android.os.ResultReceiver;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
/**
|
||||
* Listener to handle media key.
|
||||
* @hide
|
||||
*/
|
||||
interface IOnMediaKeyListener {
|
||||
void onMediaKey(in KeyEvent event, in ResultReceiver result);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.content.ComponentName;
|
||||
import android.media.IRemoteVolumeController;
|
||||
import android.media.session.IActiveSessionsListener;
|
||||
import android.media.session.IOnVolumeKeyLongPressListener;
|
||||
import android.media.session.IOnMediaKeyListener;
|
||||
import android.media.session.ISession;
|
||||
import android.media.session.ISessionCallback;
|
||||
import android.os.Bundle;
|
||||
@@ -45,5 +46,6 @@ interface ISessionManager {
|
||||
boolean isGlobalPriorityActive();
|
||||
|
||||
void setOnVolumeKeyLongPressListener(in IOnVolumeKeyLongPressListener listener);
|
||||
void setOnMediaKeyListener(in IOnMediaKeyListener listener);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.media.session.ISessionManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ResultReceiver;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.UserHandle;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
@@ -51,6 +52,18 @@ import java.util.List;
|
||||
public final class MediaSessionManager {
|
||||
private static final String TAG = "SessionManager";
|
||||
|
||||
/**
|
||||
* Used by IOnMediaKeyListener to indicate that the media key event isn't handled.
|
||||
* @hide
|
||||
*/
|
||||
public static final int RESULT_MEDIA_KEY_NOT_HANDLED = 0;
|
||||
|
||||
/**
|
||||
* Used by IOnMediaKeyListener to indicate that the media key event is handled.
|
||||
* @hide
|
||||
*/
|
||||
public static final int RESULT_MEDIA_KEY_HANDLED = 1;
|
||||
|
||||
private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners
|
||||
= new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>();
|
||||
private final Object mLock = new Object();
|
||||
@@ -59,6 +72,7 @@ public final class MediaSessionManager {
|
||||
private Context mContext;
|
||||
|
||||
private OnVolumeKeyLongPressListenerImpl mOnVolumeKeyLongPressListener;
|
||||
private OnMediaKeyListenerImpl mOnMediaKeyListener;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
@@ -363,6 +377,39 @@ public final class MediaSessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media key listener. While the listener is set, the listener
|
||||
* gets the media key before any other media sessions but after the global priority session.
|
||||
* If the listener handles the key (i.e. returns {@code true}),
|
||||
* other sessions will not get the event.
|
||||
*
|
||||
* <p>System can only have a single media key listener.
|
||||
*
|
||||
* @param listener The media key listener. {@code null} to reset.
|
||||
* @param handler The handler on which the listener should be invoked, or {@code null}
|
||||
* if the listener should be invoked on the calling thread's looper.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void setOnMediaKeyListener(OnMediaKeyListener listener, @Nullable Handler handler) {
|
||||
synchronized (mLock) {
|
||||
try {
|
||||
if (listener == null) {
|
||||
mOnMediaKeyListener = null;
|
||||
mService.setOnMediaKeyListener(null);
|
||||
} else {
|
||||
if (handler == null) {
|
||||
handler = new Handler();
|
||||
}
|
||||
mOnMediaKeyListener = new OnMediaKeyListenerImpl(listener, handler);
|
||||
mService.setOnMediaKeyListener(mOnMediaKeyListener);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Failed to set media key listener", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for changes to the list of active sessions. This can be added
|
||||
* using {@link #addOnActiveSessionsChangedListener}.
|
||||
@@ -384,6 +431,20 @@ public final class MediaSessionManager {
|
||||
void onVolumeKeyLongPress(KeyEvent event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens the media key.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public interface OnMediaKeyListener {
|
||||
/**
|
||||
* Called when the media key is pressed.
|
||||
* <p>If it takes more than 1s to return, the key event will be sent to
|
||||
* other media sessions.
|
||||
*/
|
||||
boolean onMediaKey(KeyEvent event);
|
||||
}
|
||||
|
||||
private static final class SessionsChangedWrapper {
|
||||
private Context mContext;
|
||||
private OnActiveSessionsChangedListener mListener;
|
||||
@@ -451,4 +512,33 @@ public final class MediaSessionManager {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OnMediaKeyListenerImpl extends IOnMediaKeyListener.Stub {
|
||||
private OnMediaKeyListener mListener;
|
||||
private Handler mHandler;
|
||||
|
||||
public OnMediaKeyListenerImpl(OnMediaKeyListener listener, Handler handler) {
|
||||
mListener = listener;
|
||||
mHandler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaKey(KeyEvent event, ResultReceiver result) {
|
||||
if (mListener == null || mHandler == null) {
|
||||
Log.w(TAG, "Failed to call media key listener." +
|
||||
" Either mListener or mHandler is null");
|
||||
return;
|
||||
}
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean handled = mListener.onMediaKey(event);
|
||||
Log.d(TAG, "The media key listener is returned " + handled);
|
||||
result.send(
|
||||
handled ? RESULT_MEDIA_KEY_HANDLED : RESULT_MEDIA_KEY_NOT_HANDLED,
|
||||
null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +37,13 @@ import android.media.AudioSystem;
|
||||
import android.media.IAudioService;
|
||||
import android.media.IRemoteVolumeController;
|
||||
import android.media.session.IActiveSessionsListener;
|
||||
import android.media.session.IOnMediaKeyListener;
|
||||
import android.media.session.IOnVolumeKeyLongPressListener;
|
||||
import android.media.session.ISession;
|
||||
import android.media.session.ISessionCallback;
|
||||
import android.media.session.ISessionManager;
|
||||
import android.media.session.MediaSession;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
@@ -84,6 +86,7 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
private static final boolean DEBUG_KEY_EVENT = true;
|
||||
|
||||
private static final int WAKELOCK_TIMEOUT = 5000;
|
||||
private static final int MEDIA_KEY_LISTENER_TIMEOUT = 1000;
|
||||
|
||||
/* package */final IBinder mICallback = new Binder();
|
||||
|
||||
@@ -550,6 +553,9 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
private int mInitialDownVolumeStream;
|
||||
private boolean mInitialDownMusicOnly;
|
||||
|
||||
private IOnMediaKeyListener mOnMediaKeyListener;
|
||||
private int mOnMediaKeyListenerUid;
|
||||
|
||||
public UserRecord(Context context, int userId) {
|
||||
mContext = context;
|
||||
mUserId = userId;
|
||||
@@ -583,6 +589,9 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
pw.println(indent + "Volume key long-press listener:" + mOnVolumeKeyLongPressListener);
|
||||
pw.println(indent + "Volume key long-press listener package:" +
|
||||
getCallingPackageName(mOnVolumeKeyLongPressListenerUid));
|
||||
pw.println(indent + "Media key listener: " + mOnMediaKeyListener);
|
||||
pw.println(indent + "Media key listener package: " +
|
||||
getCallingPackageName(mOnMediaKeyListenerUid));
|
||||
int size = mSessions.size();
|
||||
pw.println(indent + size + " Sessions:");
|
||||
for (int i = 0; i < size; i++) {
|
||||
@@ -788,28 +797,10 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
}
|
||||
|
||||
synchronized (mLock) {
|
||||
// If we don't have a media button receiver to fall back on
|
||||
// include non-playing sessions for dispatching
|
||||
boolean useNotPlayingSessions = true;
|
||||
for (int userId : mCurrentUserIdList) {
|
||||
UserRecord ur = mUserRecords.get(userId);
|
||||
if (ur.mLastMediaButtonReceiver != null
|
||||
|| ur.mRestoredMediaButtonReceiver != null) {
|
||||
useNotPlayingSessions = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispatchMediaKeyEvent, useNotPlayingSessions="
|
||||
+ useNotPlayingSessions);
|
||||
}
|
||||
MediaSessionRecord session = mPriorityStack.getDefaultMediaButtonSession(
|
||||
mCurrentUserIdList, useNotPlayingSessions);
|
||||
if (isVoiceKey(keyEvent.getKeyCode())) {
|
||||
handleVoiceKeyEventLocked(keyEvent, needWakeLock, session);
|
||||
if (!isGlobalPriorityActive() && isVoiceKey(keyEvent.getKeyCode())) {
|
||||
handleVoiceKeyEventLocked(keyEvent, needWakeLock);
|
||||
} else {
|
||||
dispatchMediaKeyEventLocked(keyEvent, needWakeLock, session);
|
||||
dispatchMediaKeyEventLocked(keyEvent, needWakeLock, true);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -868,6 +859,56 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnMediaKeyListener(IOnMediaKeyListener listener) {
|
||||
final int pid = Binder.getCallingPid();
|
||||
final int uid = Binder.getCallingUid();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
// Enforce SET_MEDIA_KEY_LISTENER permission.
|
||||
if (getContext().checkPermission(
|
||||
android.Manifest.permission.SET_MEDIA_KEY_LISTENER, pid, uid)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
throw new SecurityException("Must hold the SET_MEDIA_KEY_LISTENER" +
|
||||
" permission.");
|
||||
}
|
||||
|
||||
synchronized (mLock) {
|
||||
int userId = UserHandle.getUserId(uid);
|
||||
UserRecord user = mUserRecords.get(userId);
|
||||
if (user.mOnMediaKeyListener != null && user.mOnMediaKeyListenerUid != uid) {
|
||||
Log.w(TAG, "Media key listener cannot be reset by another app");
|
||||
return;
|
||||
}
|
||||
|
||||
user.mOnMediaKeyListener = listener;
|
||||
user.mOnMediaKeyListenerUid = uid;
|
||||
|
||||
Log.d(TAG, "Media key listener " + user.mOnMediaKeyListener
|
||||
+ " is set by " + getCallingPackageName(uid));
|
||||
|
||||
if (user.mOnMediaKeyListener != null) {
|
||||
try {
|
||||
user.mOnMediaKeyListener.asBinder().linkToDeath(
|
||||
new IBinder.DeathRecipient() {
|
||||
@Override
|
||||
public void binderDied() {
|
||||
synchronized (mLock) {
|
||||
user.mOnMediaKeyListener = null;
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to set death recipient " + user.mOnMediaKeyListener);
|
||||
user.mOnMediaKeyListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the dispatching of the volume button events to one of the
|
||||
* registered listeners. If there's a volume key long-press listener and
|
||||
@@ -1127,13 +1168,7 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleVoiceKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock,
|
||||
MediaSessionRecord session) {
|
||||
if (session != null && session.hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)) {
|
||||
// If the phone app has priority just give it the event
|
||||
dispatchMediaKeyEventLocked(keyEvent, needWakeLock, session);
|
||||
return;
|
||||
}
|
||||
private void handleVoiceKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock) {
|
||||
int action = keyEvent.getAction();
|
||||
boolean isLongPress = (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0;
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
@@ -1150,15 +1185,52 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
if (!mVoiceButtonHandled && !keyEvent.isCanceled()) {
|
||||
// Resend the down then send this event through
|
||||
KeyEvent downEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_DOWN);
|
||||
dispatchMediaKeyEventLocked(downEvent, needWakeLock, session);
|
||||
dispatchMediaKeyEventLocked(keyEvent, needWakeLock, session);
|
||||
dispatchMediaKeyEventLocked(downEvent, needWakeLock, true);
|
||||
dispatchMediaKeyEventLocked(keyEvent, needWakeLock, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchMediaKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock,
|
||||
MediaSessionRecord session) {
|
||||
boolean checkMediaKeyListener) {
|
||||
// If we don't have a media button receiver to fall back on
|
||||
// include non-playing sessions for dispatching.
|
||||
boolean useNotPlayingSessions = true;
|
||||
for (int userId : mCurrentUserIdList) {
|
||||
UserRecord ur = mUserRecords.get(userId);
|
||||
if (ur.mLastMediaButtonReceiver != null
|
||||
|| ur.mRestoredMediaButtonReceiver != null) {
|
||||
useNotPlayingSessions = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispatchMediaKeyEvent, useNotPlayingSessions="
|
||||
+ useNotPlayingSessions);
|
||||
}
|
||||
|
||||
MediaSessionRecord session = mPriorityStack.getDefaultMediaButtonSession(
|
||||
mCurrentUserIdList, useNotPlayingSessions);
|
||||
|
||||
if ((session == null
|
||||
|| !session.hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY))
|
||||
&& checkMediaKeyListener) {
|
||||
// Only consider full user.
|
||||
UserRecord user = mUserRecords.get(mCurrentUserIdList.get(0));
|
||||
if (user.mOnMediaKeyListener != null) {
|
||||
if (DEBUG_KEY_EVENT) {
|
||||
Log.d(TAG, "Send " + keyEvent + " to media key listener");
|
||||
}
|
||||
try {
|
||||
user.mOnMediaKeyListener.onMediaKey(keyEvent,
|
||||
new MediaKeyListenerResultReceiver(keyEvent, needWakeLock));
|
||||
return;
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to send " + keyEvent + " to media key listener");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (session != null) {
|
||||
if (DEBUG_KEY_EVENT) {
|
||||
Log.d(TAG, "Sending " + keyEvent + " to " + session);
|
||||
@@ -1282,6 +1354,46 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
&& streamType <= AudioManager.STREAM_NOTIFICATION;
|
||||
}
|
||||
|
||||
private class MediaKeyListenerResultReceiver extends ResultReceiver implements Runnable {
|
||||
private KeyEvent mKeyEvent;
|
||||
private boolean mNeedWakeLock;
|
||||
private boolean mHandled;
|
||||
|
||||
private MediaKeyListenerResultReceiver(KeyEvent keyEvent, boolean needWakeLock) {
|
||||
super(mHandler);
|
||||
mHandler.postDelayed(this, MEDIA_KEY_LISTENER_TIMEOUT);
|
||||
mKeyEvent = keyEvent;
|
||||
mNeedWakeLock = needWakeLock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(TAG, "The media key listener is timed-out for " + mKeyEvent);
|
||||
dispatchMediaKeyEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
if (resultCode == MediaSessionManager.RESULT_MEDIA_KEY_HANDLED) {
|
||||
mHandled = true;
|
||||
mHandler.removeCallbacks(this);
|
||||
return;
|
||||
}
|
||||
dispatchMediaKeyEvent();
|
||||
}
|
||||
|
||||
private void dispatchMediaKeyEvent() {
|
||||
if (mHandled) {
|
||||
return;
|
||||
}
|
||||
mHandled = true;
|
||||
mHandler.removeCallbacks(this);
|
||||
synchronized (mLock) {
|
||||
dispatchMediaKeyEventLocked(mKeyEvent, mNeedWakeLock, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private KeyEventWakeLockReceiver mKeyEventReceiver = new KeyEventWakeLockReceiver(mHandler);
|
||||
|
||||
class KeyEventWakeLockReceiver extends ResultReceiver implements Runnable,
|
||||
|
||||
Reference in New Issue
Block a user