summaryrefslogtreecommitdiffstats
path: root/media
diff options
context:
space:
mode:
Diffstat (limited to 'media')
-rw-r--r--media/java/android/media/AudioManager.java84
-rw-r--r--media/java/android/media/AudioService.java104
-rw-r--r--media/java/android/media/IAudioService.aidl10
-rw-r--r--media/java/android/media/ImageReader.java68
-rw-r--r--media/java/android/media/MediaFocusControl.java43
-rw-r--r--media/java/android/media/MediaFormat.java36
-rw-r--r--media/java/android/media/MediaMetadataEditor.java2
-rw-r--r--media/java/android/media/MediaPlayer.java49
-rw-r--r--media/java/android/media/Rating.java10
-rw-r--r--media/java/android/media/RemoteControlClient.java6
-rw-r--r--media/java/android/media/RemoteController.java796
-rw-r--r--media/java/android/media/SubtitleController.java179
-rw-r--r--media/java/android/media/SubtitleTrack.java6
13 files changed, 1214 insertions, 179 deletions
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 845baaf..a4009cf 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -323,6 +323,12 @@ public class AudioManager {
public static final int FLAG_FIXED_VOLUME = 1 << 5;
/**
+ * Indicates the volume set/adjust call is for Bluetooth absolute volume
+ * @hide
+ */
+ public static final int FLAG_BLUETOOTH_ABS_VOLUME = 1 << 6;
+
+ /**
* Ringer mode that will be silent and will not vibrate. (This overrides the
* vibrate setting.)
*
@@ -438,6 +444,19 @@ public class AudioManager {
/**
* @hide
+ * @param KeyEvent
+ */
+ protected void dispatchMediaKeyEvent(KeyEvent keyEvent) {
+ IAudioService service = getService();
+ try {
+ service.dispatchMediaKeyEvent(keyEvent);
+ } catch (RemoteException e) {
+ Log.e(TAG, "dispatchMediaKeyEvent threw exception ", e);
+ }
+ }
+
+ /**
+ * @hide
*/
public void preDispatchKeyEvent(KeyEvent event, int stream) {
/*
@@ -2229,6 +2248,49 @@ public class AudioManager {
/**
* @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param rctlr
+ * @return true if the {@link RemoteController} was successfully registered, false if an
+ * error occurred, due to an internal system error, or insufficient permissions.
+ */
+ public boolean registerRemoteController(RemoteController rctlr) {
+ if (rctlr == null) {
+ return false;
+ }
+ IAudioService service = getService();
+ try {
+ boolean reg = service.registerRemoteControlDisplay(rctlr.getRcDisplay(),
+ // passing a negative value for art work width and height
+ // as they are still unknown at this stage
+ /*w*/-1, /*h*/ -1);
+ rctlr.setIsRegistered(reg);
+ return reg;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in registerRemoteControlDisplay " + e);
+ return false;
+ }
+ }
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param rctlr
+ */
+ public void unregisterRemoteController(RemoteController rctlr) {
+ if (rctlr == null) {
+ return;
+ }
+ IAudioService service = getService();
+ try {
+ service.unregisterRemoteControlDisplay(rctlr.getRcDisplay());
+ rctlr.setIsRegistered(false);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in unregisterRemoteControlDisplay " + e);
+ }
+ }
+
+ /**
+ * @hide
* Registers a remote control display that will be sent information by remote control clients.
* Use this method if your IRemoteControlDisplay is not going to display artwork, otherwise
* use {@link #registerRemoteControlDisplay(IRemoteControlDisplay, int, int)} to pass the
@@ -2257,8 +2319,6 @@ public class AudioManager {
}
IAudioService service = getService();
try {
- // passing a negative value for art work width and height as they are unknown at
- // this stage
service.registerRemoteControlDisplay(rcd, w, h);
} catch (RemoteException e) {
Log.e(TAG, "Dead object in registerRemoteControlDisplay " + e);
@@ -2351,13 +2411,15 @@ public class AudioManager {
/**
* @hide
- * Notify the user of a RemoteControlClient that it should update its metadata
+ * Notify the user of a RemoteControlClient that it should update its metadata with the
+ * new value for the given key.
* @param generationId the RemoteControlClient generation counter for which this request is
* issued. Requests for an older generation than current one will be ignored.
* @param key the metadata key for which a new value exists
* @param value the new metadata value
*/
- public void updateRemoteControlClientMetadata(int generationId, int key, long value) {
+ public void updateRemoteControlClientMetadata(int generationId, int key,
+ Rating value) {
IAudioService service = getService();
try {
service.updateRemoteControlClientMetadata(generationId, key, value);
@@ -2397,20 +2459,6 @@ public class AudioManager {
}
}
- /**
- * @hide
- * Notifies AudioService of the volume set on the A2DP device as a callback, so AudioService
- * is able to update the UI.
- */
- public void avrcpUpdateVolume(int oldVolume, int volume) {
- IAudioService service = getService();
- try {
- service.avrcpUpdateVolume(oldVolume, volume);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in avrcpUpdateVolume", e);
- }
- }
-
/**
* {@hide}
*/
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index b72551a..3425c91 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -843,6 +843,13 @@ public class AudioService extends IAudioService.Stub {
boolean adjustVolume = true;
int step;
+ // skip a2dp absolute volume control request when the device
+ // is not an a2dp device
+ if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) == 0 &&
+ (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) {
+ return;
+ }
+
if (mAppOps.noteOp(STEAM_VOLUME_OPS[streamTypeAlias], Binder.getCallingUid(),
callingPackage) != AppOpsManager.MODE_ALLOWED) {
return;
@@ -892,15 +899,18 @@ public class AudioService extends IAudioService.Stub {
int oldIndex = mStreamStates[streamType].getIndex(device);
if (adjustVolume && (direction != AudioManager.ADJUST_SAME)) {
+
// Check if volume update should be send to AVRCP
- synchronized (mA2dpAvrcpLock) {
- if (mA2dp != null && mAvrcpAbsVolSupported) {
- mA2dp.adjustAvrcpAbsoluteVolume(direction);
- return;
- // No need to send volume update, because we will update the volume with a
- // callback from Avrcp.
+ if (streamTypeAlias == AudioSystem.STREAM_MUSIC &&
+ (device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 &&
+ (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
+ synchronized (mA2dpAvrcpLock) {
+ if (mA2dp != null && mAvrcpAbsVolSupported) {
+ mA2dp.adjustAvrcpAbsoluteVolume(direction);
+ }
}
}
+
if ((direction == AudioManager.ADJUST_RAISE) &&
!checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
Log.e(TAG, "adjustStreamVolume() safe volume index = "+oldIndex);
@@ -985,6 +995,13 @@ public class AudioService extends IAudioService.Stub {
final int device = getDeviceForStream(streamType);
int oldIndex;
+ // skip a2dp absolute volume control request when the device
+ // is not an a2dp device
+ if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) == 0 &&
+ (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) {
+ return;
+ }
+
if (mAppOps.noteOp(STEAM_VOLUME_OPS[streamTypeAlias], Binder.getCallingUid(),
callingPackage) != AppOpsManager.MODE_ALLOWED) {
return;
@@ -998,12 +1015,13 @@ public class AudioService extends IAudioService.Stub {
index = rescaleIndex(index * 10, streamType, streamTypeAlias);
- synchronized (mA2dpAvrcpLock) {
- if (mA2dp != null && mAvrcpAbsVolSupported) {
- mA2dp.setAvrcpAbsoluteVolume(index);
- return;
- // No need to send volume update, because we will update the volume with a
- // callback from Avrcp.
+ if (streamTypeAlias == AudioSystem.STREAM_MUSIC &&
+ (device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 &&
+ (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
+ synchronized (mA2dpAvrcpLock) {
+ if (mA2dp != null && mAvrcpAbsVolSupported) {
+ mA2dp.setAvrcpAbsoluteVolume(index);
+ }
}
}
@@ -2835,7 +2853,12 @@ public class AudioService extends IAudioService.Stub {
int index;
if (isMuted()) {
index = 0;
- } else {
+ } else if (mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_MUSIC &&
+ (device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 &&
+ mAvrcpAbsVolSupported) {
+ index = (mIndexMax + 5)/10;
+ }
+ else {
index = (getIndex(device) + 5)/10;
}
AudioSystem.setStreamVolumeIndex(mStreamType, index, device);
@@ -3650,6 +3673,9 @@ public class AudioService extends IAudioService.Stub {
private void makeA2dpDeviceAvailable(String address) {
// enable A2DP before notifying A2DP connection to avoid unecessary processing in
// audio policy manager
+ VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
+ sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE,
+ AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0, streamState, 0);
setBluetoothA2dpOnInt(true);
AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
AudioSystem.DEVICE_STATE_AVAILABLE,
@@ -3666,6 +3692,9 @@ public class AudioService extends IAudioService.Stub {
// must be called synchronized on mConnectedDevices
private void makeA2dpDeviceUnavailableNow(String address) {
+ synchronized (mA2dpAvrcpLock) {
+ mAvrcpAbsVolSupported = false;
+ }
AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
address);
@@ -3706,19 +3735,6 @@ public class AudioService extends IAudioService.Stub {
address = "";
}
- // Disable absolute volume, if device is disconnected
- synchronized (mA2dpAvrcpLock) {
- if (state == BluetoothProfile.STATE_DISCONNECTED && mAvrcpAbsVolSupported) {
- mAvrcpAbsVolSupported = false;
- sendMsg(mAudioHandler,
- MSG_SET_DEVICE_VOLUME,
- SENDMSG_QUEUE,
- getDeviceForStream(AudioSystem.STREAM_MUSIC),
- 0,
- mStreamStates[AudioSystem.STREAM_MUSIC],
- 0);
- }
- }
synchronized (mConnectedDevices) {
boolean isConnected =
(mConnectedDevices.containsKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) &&
@@ -3773,27 +3789,12 @@ public class AudioService extends IAudioService.Stub {
// address is not used for now, but may be used when multiple a2dp devices are supported
synchronized (mA2dpAvrcpLock) {
mAvrcpAbsVolSupported = support;
- if (support) {
- VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
- int device = getDeviceForStream(AudioSystem.STREAM_MUSIC);
- streamState.setIndex(streamState.getMaxIndex(), device);
- sendMsg(mAudioHandler,
- MSG_SET_DEVICE_VOLUME,
- SENDMSG_QUEUE,
- device,
- 0,
- streamState,
- 0);
- }
+ VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
+ sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE,
+ AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0, streamState, 0);
}
}
- public void avrcpUpdateVolume(int oldVolume, int volume) {
- mStreamStates[AudioSystem.STREAM_MUSIC].
- setIndex(volume, getDeviceForStream(AudioSystem.STREAM_MUSIC));
- sendVolumeUpdate(AudioSystem.STREAM_MUSIC, oldVolume, volume, AudioManager.FLAG_SHOW_UI);
- }
-
private boolean handleDeviceConnection(boolean connected, int device, String params) {
synchronized (mConnectedDevices) {
boolean isConnected = (mConnectedDevices.containsKey(device) &&
@@ -4142,8 +4143,17 @@ public class AudioService extends IAudioService.Stub {
//==========================================================================================
// RemoteControlDisplay / RemoteControlClient / Remote info
//==========================================================================================
- public void registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) {
- mMediaFocusControl.registerRemoteControlDisplay(rcd, w, h);
+ public boolean registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) {
+ if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.MEDIA_CONTENT_CONTROL)) {
+ mMediaFocusControl.registerRemoteControlDisplay(rcd, w, h);
+ return true;
+ } else {
+ Log.w(TAG, "Access denied to process: " + Binder.getCallingPid() +
+ ", must have permission " + android.Manifest.permission.MEDIA_CONTENT_CONTROL +
+ " to register IRemoteControlDisplay");
+ return false;
+ }
}
public void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) {
@@ -4189,7 +4199,7 @@ public class AudioService extends IAudioService.Stub {
mMediaFocusControl.setRemoteControlClientPlaybackPosition(generationId, timeMs);
}
- public void updateRemoteControlClientMetadata(int generationId, int key, long value) {
+ public void updateRemoteControlClientMetadata(int generationId, int key, Rating value) {
mMediaFocusControl.updateRemoteControlClientMetadata(generationId, key, value);
}
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index fe060f8..e3b87dd 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -26,6 +26,7 @@ import android.media.IRemoteControlClient;
import android.media.IRemoteControlDisplay;
import android.media.IRemoteVolumeObserver;
import android.media.IRingtonePlayer;
+import android.media.Rating;
import android.net.Uri;
import android.view.KeyEvent;
@@ -100,8 +101,6 @@ interface IAudioService {
oneway void avrcpSupportsAbsoluteVolume(String address, boolean support);
- oneway void avrcpUpdateVolume(int oldVolume, int volume);
-
void setSpeakerphoneOn(boolean on);
boolean isSpeakerphoneOn();
@@ -142,7 +141,7 @@ interface IAudioService {
* @param h the maximum height of the expected bitmap. Negative or zero values indicate this
* display doesn't need to receive artwork.
*/
- oneway void registerRemoteControlDisplay(in IRemoteControlDisplay rcd, int w, int h);
+ boolean registerRemoteControlDisplay(in IRemoteControlDisplay rcd, int w, int h);
/**
* Unregister an IRemoteControlDisplay.
* No effect if the IRemoteControlDisplay hasn't been successfully registered.
@@ -180,13 +179,14 @@ interface IAudioService {
*/
void setRemoteControlClientPlaybackPosition(int generationId, long timeMs);
/**
- * Notify the user of a RemoteControlClient that it should update its metadata
+ * Notify the user of a RemoteControlClient that it should update its metadata with the
+ * new value for the given key.
* @param generationId the RemoteControlClient generation counter for which this request is
* issued. Requests for an older generation than current one will be ignored.
* @param key the metadata key for which a new value exists
* @param value the new metadata value
*/
- void updateRemoteControlClientMetadata(int generationId, int key, long value);
+ void updateRemoteControlClientMetadata(int generationId, int key, in Rating value);
/**
* Do not use directly, use instead
diff --git a/media/java/android/media/ImageReader.java b/media/java/android/media/ImageReader.java
index aee8362..f9e48d4 100644
--- a/media/java/android/media/ImageReader.java
+++ b/media/java/android/media/ImageReader.java
@@ -20,6 +20,7 @@ import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.Looper;
+import android.os.Message;
import android.view.Surface;
import java.lang.ref.WeakReference;
@@ -377,17 +378,21 @@ public class ImageReader implements AutoCloseable {
* @throws IllegalArgumentException
* If no handler specified and the calling thread has no looper.
*/
- public void setOnImageAvailableListener(OnImageAvailableListener listener, Handler handler) {
- mImageListener = listener;
-
- Looper looper;
- mHandler = handler;
- if (listener != null && mHandler == null) {
- if ((looper = Looper.myLooper()) != null) {
- mHandler = new Handler();
+ public void setOnImageAvailableListener(OnImageAvailableListener listener, Handler handler) {
+ synchronized (mListenerLock) {
+ if (listener != null) {
+ Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
+ if (looper == null) {
+ throw new IllegalArgumentException(
+ "handler is null but the current thread is not a looper");
+ }
+ if (mListenerHandler == null || mListenerHandler.getLooper() != looper) {
+ mListenerHandler = new ListenerHandler(looper);
+ }
+ mListener = listener;
} else {
- throw new IllegalArgumentException(
- "Looper doesn't exist in the calling thread");
+ mListener = null;
+ mListenerHandler = null;
}
}
}
@@ -426,6 +431,7 @@ public class ImageReader implements AutoCloseable {
*/
@Override
public void close() {
+ setOnImageAvailableListener(null, null);
nativeClose();
}
@@ -474,6 +480,9 @@ public class ImageReader implements AutoCloseable {
/**
* Called from Native code when an Event happens.
+ *
+ * This may be called from an arbitrary Binder thread, so access to the ImageReader must be
+ * synchronized appropriately.
*/
private static void postEventFromNative(Object selfRef) {
@SuppressWarnings("unchecked")
@@ -483,16 +492,16 @@ public class ImageReader implements AutoCloseable {
return;
}
- if (ir.mHandler != null && ir.mImageListener != null) {
- ir.mHandler.post(new Runnable() {
- @Override
- public void run() {
- ir.mImageListener.onImageAvailable(ir);
- }
- });
+ final Handler handler;
+ synchronized (ir.mListenerLock) {
+ handler = ir.mListenerHandler;
+ }
+ if (handler != null) {
+ handler.sendEmptyMessage(0);
}
}
+
private final int mWidth;
private final int mHeight;
private final int mFormat;
@@ -500,14 +509,35 @@ public class ImageReader implements AutoCloseable {
private final int mNumPlanes;
private final Surface mSurface;
- private Handler mHandler;
- private OnImageAvailableListener mImageListener;
+ private final Object mListenerLock = new Object();
+ private OnImageAvailableListener mListener;
+ private ListenerHandler mListenerHandler;
/**
* This field is used by native code, do not access or modify.
*/
private long mNativeContext;
+ /**
+ * This custom handler runs asynchronously so callbacks don't get queued behind UI messages.
+ */
+ private final class ListenerHandler extends Handler {
+ public ListenerHandler(Looper looper) {
+ super(looper, null, true /*async*/);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ OnImageAvailableListener listener;
+ synchronized (mListenerLock) {
+ listener = mListener;
+ }
+ if (listener != null) {
+ listener.onImageAvailable(ImageReader.this);
+ }
+ }
+ }
+
private class SurfaceImage extends android.media.Image {
public SurfaceImage() {
mIsImageValid = false;
diff --git a/media/java/android/media/MediaFocusControl.java b/media/java/android/media/MediaFocusControl.java
index 34dc580..143ddf2 100644
--- a/media/java/android/media/MediaFocusControl.java
+++ b/media/java/android/media/MediaFocusControl.java
@@ -138,7 +138,7 @@ public class MediaFocusControl implements OnFinished {
private static final int MSG_PROMOTE_RCC = 6;
private static final int MSG_RCC_NEW_PLAYBACK_STATE = 7;
private static final int MSG_RCC_SEEK_REQUEST = 8;
- private static final int MSG_RCC_UPDATE_METADATA_LONG = 9;
+ private static final int MSG_RCC_UPDATE_METADATA = 9;
// sendMsg() flags
/** If the msg is already queued, replace it with this one. */
@@ -206,9 +206,9 @@ public class MediaFocusControl implements OnFinished {
msg.arg1 /* generationId */, ((Long)msg.obj).longValue() /* timeMs */);
break;
- case MSG_RCC_UPDATE_METADATA_LONG:
- onUpdateRemoteControlClientMetadataLong(msg.arg1 /*genId*/, msg.arg2 /*key*/,
- ((Long)msg.obj).longValue() /* value */);
+ case MSG_RCC_UPDATE_METADATA:
+ onUpdateRemoteControlClientMetadata(msg.arg1 /*genId*/, msg.arg2 /*key*/,
+ (Rating) msg.obj /* value */);
break;
case MSG_PROMOTE_RCC:
@@ -720,11 +720,7 @@ public class MediaFocusControl implements OnFinished {
}
}
- private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
- if (keyEvent == null) {
- return false;
- }
- final int keyCode = keyEvent.getKeyCode();
+ protected static boolean isMediaKeyCode(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_MUTE:
case KeyEvent.KEYCODE_HEADSETHOOK:
@@ -740,11 +736,17 @@ public class MediaFocusControl implements OnFinished {
case KeyEvent.KEYCODE_MEDIA_CLOSE:
case KeyEvent.KEYCODE_MEDIA_EJECT:
case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
- break;
+ return true;
default:
return false;
}
- return true;
+ }
+
+ private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
+ if (keyEvent == null) {
+ return false;
+ }
+ return MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode());
}
/**
@@ -2080,24 +2082,21 @@ public class MediaFocusControl implements OnFinished {
}
}
- protected void updateRemoteControlClientMetadata(int genId, int key, long value) {
- sendMsg(mEventHandler, MSG_RCC_UPDATE_METADATA_LONG, SENDMSG_QUEUE,
- genId /* arg1 */, key /* arg2 */, Long.valueOf(value) /* obj */, 0 /* delay */);
+ protected void updateRemoteControlClientMetadata(int genId, int key, Rating value) {
+ sendMsg(mEventHandler, MSG_RCC_UPDATE_METADATA, SENDMSG_QUEUE,
+ genId /* arg1 */, key /* arg2 */, value /* obj */, 0 /* delay */);
}
- private void onUpdateRemoteControlClientMetadataLong(int genId, int key, long value) {
- if(DEBUG_RC) Log.d(TAG, "onUpdateRemoteControlClientMetadataLong(genId=" + genId +
- ", what=" + key + ",val=" + value + ")");
+ private void onUpdateRemoteControlClientMetadata(int genId, int key, Rating value) {
+ if(DEBUG_RC) Log.d(TAG, "onUpdateRemoteControlClientMetadata(genId=" + genId +
+ ", what=" + key + ",rating=" + value + ")");
synchronized(mRCStack) {
synchronized(mCurrentRcLock) {
if ((mCurrentRcClient != null) && (mCurrentRcClientGen == genId)) {
try {
switch (key) {
- case RemoteControlClient.MetadataEditor.RATING_KEY_BY_USER:
- // TODO handle rating update, placeholder code here that sends
- // an unrated percent-based rating
- mCurrentRcClient.updateMetadata(genId, key,
- Rating.newUnratedRating(Rating.RATING_PERCENTAGE));
+ case MediaMetadataEditor.RATING_KEY_BY_USER:
+ mCurrentRcClient.updateMetadata(genId, key, value);
break;
default:
Log.e(TAG, "unhandled metadata key " + key + " update for RCC "
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index 313db20..0f7906e 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -222,26 +222,36 @@ public final class MediaFormat {
public static final String KEY_FLAC_COMPRESSION_LEVEL = "flac-compression-level";
/**
- * A key for boolean AUTOSELECT field. Tracks with AUTOSELECT=true are
- * considered when automatically selecting a track without specific user
- * choice (as defined by HLS).
- * @hide
+ * A key for boolean AUTOSELECT behavior for the track. Tracks with AUTOSELECT=true
+ * are considered when automatically selecting a track without specific user
+ * choice, based on the current locale.
+ * This is currently only used for subtitle tracks, when the user selected
+ * 'Default' for the captioning locale.
+ * The associated value is an integer, where non-0 means TRUE. This is an optional
+ * field; if not specified, AUTOSELECT defaults to TRUE.
*/
- public static final String KEY_AUTOSELECT = "autoselect";
+ public static final String KEY_IS_AUTOSELECT = "is-autoselect";
/**
- * A key for boolean DEFAULT field. The track with DEFAULT=true is selected
- * in the absence of a specific user choice (as defined by HLS).
- * @hide
+ * A key for boolean DEFAULT behavior for the track. The track with DEFAULT=true is
+ * selected in the absence of a specific user choice.
+ * This is currently only used for subtitle tracks, when the user selected
+ * 'Default' for the captioning locale.
+ * The associated value is an integer, where non-0 means TRUE. This is an optional
+ * field; if not specified, DEFAULT is considered to be FALSE.
*/
- public static final String KEY_DEFAULT = "default";
+ public static final String KEY_IS_DEFAULT = "is-default";
+
/**
- * A key for boolean FORCED field for subtitle tracks. True if it is a
- * forced subtitle track.
- * @hide
+ * A key for the FORCED field for subtitle tracks. True if it is a
+ * forced subtitle track. Forced subtitle tracks are essential for the
+ * content and are shown even when the user turns off Captions. They
+ * are used for example to translate foreign/alien dialogs or signs.
+ * The associated value is an integer, where non-0 means TRUE. This is an
+ * optional field; if not specified, FORCED defaults to FALSE.
*/
- public static final String KEY_FORCED = "forced";
+ public static final String KEY_IS_FORCED_SUBTITLE = "is-forced-subtitle";
/* package private */ MediaFormat(Map<String, Object> map) {
mMap = map;
diff --git a/media/java/android/media/MediaMetadataEditor.java b/media/java/android/media/MediaMetadataEditor.java
index b601016..373ba11 100644
--- a/media/java/android/media/MediaMetadataEditor.java
+++ b/media/java/android/media/MediaMetadataEditor.java
@@ -273,7 +273,7 @@ public abstract class MediaMetadataEditor {
* <li>{@link Rating} object are {@link #RATING_KEY_BY_OTHERS}
* and {@link #RATING_KEY_BY_USER}.</li>
* </ul>
- * @param obj the metadata to add.
+ * @param value the metadata to add.
* @return Returns a reference to the same MediaMetadataEditor object, so you can chain put
* calls together.
* @throws IllegalArgumentException
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index 7acf8af..deba2cc 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -1606,9 +1606,9 @@ public class MediaPlayer implements SubtitleController.Listener
} else if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
mFormat = MediaFormat.createSubtitleFormat(
MEDIA_MIMETYPE_TEXT_VTT, language);
- mFormat.setInteger(MediaFormat.KEY_AUTOSELECT, in.readInt());
- mFormat.setInteger(MediaFormat.KEY_DEFAULT, in.readInt());
- mFormat.setInteger(MediaFormat.KEY_FORCED, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
} else {
mFormat = new MediaFormat();
mFormat.setString(MediaFormat.KEY_LANGUAGE, language);
@@ -1638,9 +1638,9 @@ public class MediaPlayer implements SubtitleController.Listener
dest.writeString(getLanguage());
if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_AUTOSELECT));
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_DEFAULT));
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_FORCED));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
}
}
@@ -1765,15 +1765,21 @@ public class MediaPlayer implements SubtitleController.Listener
@Override
public void onSubtitleTrackSelected(SubtitleTrack track) {
if (mSelectedSubtitleTrackIndex >= 0) {
- deselectTrack(mSelectedSubtitleTrackIndex);
+ try {
+ selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, false);
+ } catch (IllegalStateException e) {
+ }
+ mSelectedSubtitleTrackIndex = -1;
}
- mSelectedSubtitleTrackIndex = -1;
setOnSubtitleDataListener(null);
for (int i = 0; i < mInbandSubtitleTracks.length; i++) {
if (mInbandSubtitleTracks[i] == track) {
Log.v(TAG, "Selecting subtitle track " + i);
- selectTrack(i);
mSelectedSubtitleTrackIndex = i;
+ try {
+ selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true);
+ } catch (IllegalStateException e) {
+ }
setOnSubtitleDataListener(mSubtitleDataListener);
break;
}
@@ -2046,13 +2052,30 @@ public class MediaPlayer implements SubtitleController.Listener
private void selectOrDeselectTrack(int index, boolean select)
throws IllegalStateException {
- // ignore out-of-band tracks
- TrackInfo[] trackInfo = getInbandTrackInfo();
- if (index >= trackInfo.length &&
- index < trackInfo.length + mOutOfBandSubtitleTracks.size()) {
+ // handle subtitle track through subtitle controller
+ SubtitleTrack track = null;
+ if (index < mInbandSubtitleTracks.length) {
+ track = mInbandSubtitleTracks[index];
+ } else if (index < mInbandSubtitleTracks.length + mOutOfBandSubtitleTracks.size()) {
+ track = mOutOfBandSubtitleTracks.get(index - mInbandSubtitleTracks.length);
+ }
+
+ if (mSubtitleController != null && track != null) {
+ if (select) {
+ mSubtitleController.selectTrack(track);
+ } else if (mSubtitleController.getSelectedTrack() == track) {
+ mSubtitleController.selectTrack(null);
+ } else {
+ Log.w(TAG, "trying to deselect track that was not selected");
+ }
return;
}
+ selectOrDeselectInbandTrack(index, select);
+ }
+
+ private void selectOrDeselectInbandTrack(int index, boolean select)
+ throws IllegalStateException {
Parcel request = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
diff --git a/media/java/android/media/Rating.java b/media/java/android/media/Rating.java
index 48443ff..82c0392 100644
--- a/media/java/android/media/Rating.java
+++ b/media/java/android/media/Rating.java
@@ -75,6 +75,16 @@ public final class Rating implements Parcelable {
mRatingValue = rating;
}
+
+ /**
+ * @hide
+ */
+ @Override
+ public String toString () {
+ return "Rating:style=" + mRatingStyle + " rating="
+ + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+ }
+
@Override
public int describeContents() {
return mRatingStyle;
diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java
index f8faf3a..7613c89 100644
--- a/media/java/android/media/RemoteControlClient.java
+++ b/media/java/android/media/RemoteControlClient.java
@@ -298,8 +298,8 @@ public class RemoteControlClient
* Flag indicating a RemoteControlClient supports ratings.
* This flag must be set in order for components that display the RemoteControlClient
* information, to display ratings information, and, if ratings are declared editable
- * (by calling {@link MetadataEditor#addEditableKey(int)} with the
- * {@link MetadataEditor#LONG_KEY_RATING_BY_USER} key), it will enable the user to rate
+ * (by calling {@link MediaMetadataEditor#addEditableKey(int)} with the
+ * {@link MediaMetadataEditor#RATING_KEY_BY_USER} key), it will enable the user to rate
* the media, with values being received through the interface set with
* {@link #setMetadataUpdateListener(OnMetadataUpdateListener)}.
* @see #setTransportControlFlags(int)
@@ -715,7 +715,7 @@ public class RemoteControlClient
* Implement this interface to receive metadata updates after registering your listener
* through {@link RemoteControlClient#setMetadataUpdateListener(OnMetadataUpdateListener)}.
*/
- public interface OnMetadataUpdateListener {
+ public interface OnMetadataUpdateListener {
/**
* Called on the implementer to notify that the metadata field for the given key has
* been updated to the new value.
diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java
new file mode 100644
index 0000000..6266160
--- /dev/null
+++ b/media/java/android/media/RemoteController.java
@@ -0,0 +1,796 @@
+/*
+ * Copyright (C) 2013 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;
+
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.IRemoteControlDisplay;
+import android.media.MediaMetadataEditor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.view.KeyEvent;
+
+/**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * The RemoteController class is used to control media playback, display and update media metadata
+ * and playback status, published by applications using the {@link RemoteControlClient} class.
+ * <p>
+ * A RemoteController shall be registered through
+ * {@link AudioManager#registerRemoteController(RemoteController)} in order for the system to send
+ * media event updates to the listener set in
+ * {@link #setOnClientUpdateListener(OnClientUpdateListener)}. This listener is a subclass of
+ * the {@link OnClientUpdateListener} abstract class. Override its methods to receive the
+ * information published by the active {@link RemoteControlClient} instances.
+ * By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for album
+ * art. Use {@link #setBitmapConfiguration(boolean, int, int)} to receive images as well.
+ * <p>
+ * A RemoteController can also be used without being registered, when it is only meant to send
+ * media key events (for play or stop events for instance),
+ * with {@link #sendMediaKeyEvent(KeyEvent)}.
+ */
+public class RemoteController
+{
+ private final static int MAX_BITMAP_DIMENSION = 512;
+ private final static int TRANSPORT_UNKNOWN = 0;
+ private RcDisplay mRcd;
+ private final static String TAG = "RemoteController";
+ private final static boolean DEBUG = false;
+ private final static Object mGenLock = new Object();
+ private final static Object mInfoLock = new Object();
+ private Context mContext;
+ private AudioManager mAudioManager;
+ private MetadataEditor mMetadataEditor;
+
+ /**
+ * Synchronized on mGenLock
+ */
+ private int mClientGenerationIdCurrent = 0;
+
+ /**
+ * Synchronized on mInfoLock
+ */
+ private boolean mIsRegistered = false;
+ private PendingIntent mClientPendingIntentCurrent;
+ private OnClientUpdateListener mOnClientUpdateListener;
+ private PlaybackInfo mLastPlaybackInfo;
+ private int mLastTransportControlFlags = TRANSPORT_UNKNOWN;
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param ctxt non-null {@link Context}
+ * @throws java.lang.IllegalArgumentException
+ */
+ public RemoteController(Context ctxt) throws IllegalArgumentException {
+ if (ctxt == null) {
+ throw new IllegalArgumentException("Invalid null Context");
+ }
+ Looper looper;
+ if ((looper = Looper.myLooper()) != null) {
+ mEventHandler = new EventHandler(this, looper);
+ } else if ((looper = Looper.getMainLooper()) != null) {
+ mEventHandler = new EventHandler(this, looper);
+ } else {
+ mEventHandler = null;
+ Log.e(TAG, "RemoteController() couldn't find main application thread");
+ }
+ mContext = ctxt;
+ mRcd = new RcDisplay();
+ mAudioManager = (AudioManager) ctxt.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param looper
+ * @param ctxt non-null {@link Context}
+ * @throws java.lang.IllegalArgumentException
+ */
+ public RemoteController(Looper looper, Context ctxt) throws IllegalArgumentException {
+ if (ctxt == null) {
+ throw new IllegalArgumentException("Invalid null Context");
+ }
+ mEventHandler = new EventHandler(this, looper);
+ mContext = ctxt;
+ mRcd = new RcDisplay();
+ mAudioManager = (AudioManager) ctxt.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ */
+ public static abstract class OnClientUpdateListener {
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param clearing
+ */
+ public void onClientReset(boolean clearing) { }
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param state
+ */
+ public void onClientPlaybackStateUpdate(int state) { }
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param state
+ * @param stateChangeTimeMs
+ * @param currentPosMs
+ * @param speed
+ */
+ public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
+ long currentPosMs, float speed) { }
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param transportControlFlags
+ * @param posCapabilities
+ */
+ public void onClientTransportControlUpdate(int transportControlFlags) { }
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param metadataEditor
+ */
+ public void onClientMetadataUpdate(MetadataEditor metadataEditor) { }
+ };
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param l
+ */
+ public void setOnClientUpdateListener(OnClientUpdateListener l) {
+ synchronized(mInfoLock) {
+ mOnClientUpdateListener = l;
+ if (!mIsRegistered) {
+ // since the object is not registered, it hasn't received any information from
+ // RemoteControlClients yet, so we can exit here.
+ return;
+ }
+ if (mLastPlaybackInfo != null) {
+ sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE,
+ mClientGenerationIdCurrent /*arg1*/, 0,
+ mLastPlaybackInfo /*obj*/, 0 /*delay*/);
+ }
+ if (mLastTransportControlFlags != TRANSPORT_UNKNOWN) {
+ sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE,
+ mClientGenerationIdCurrent /*arg1*/, mLastTransportControlFlags /*arg2*/,
+ null /*obj*/, 0 /*delay*/);
+ }
+ if (mMetadataEditor != null) {
+ sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
+ mClientGenerationIdCurrent /*arg1*/, 0 /*arg2*/,
+ mMetadataEditor /*obj*/, 0 /*delay*/);
+ }
+ }
+ }
+
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * Send a simulated key event for a media button.
+ * May be used without registering the RemoteController
+ * with {@link AudioManager#registerRemoteController(RemoteController)}. To simulate a key
+ * press, you must first send a KeyEvent built with a {@link KeyEvent#ACTION_DOWN} action, then
+ * another event with the {@link KeyEvent#ACTION_UP} action.
+ * <p> When used from a registered RemoteController, the key event will be sent to the
+ * application currently promoted to publish its media metadata and playback state (there may be
+ * none under some circumstances). With an unregistered RemoteController, the key event will be
+ * sent to the current media key event consumer
+ * (see {@link AudioManager#registerMediaButtonEventReceiver(PendingIntent)}).
+ * @param keyEvent a {@link KeyEvent} instance whose key code is one of
+ * {@link KeyEvent.KEYCODE_MUTE},
+ * {@link KeyEvent.KEYCODE_HEADSETHOOK},
+ * {@link KeyEvent.KEYCODE_MEDIA_PLAY},
+ * {@link KeyEvent.KEYCODE_MEDIA_PAUSE},
+ * {@link KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE},
+ * {@link KeyEvent.KEYCODE_MEDIA_STOP},
+ * {@link KeyEvent.KEYCODE_MEDIA_NEXT},
+ * {@link KeyEvent.KEYCODE_MEDIA_PREVIOUS},
+ * {@link KeyEvent.KEYCODE_MEDIA_REWIND},
+ * {@link KeyEvent.KEYCODE_MEDIA_RECORD},
+ * {@link KeyEvent.KEYCODE_MEDIA_FAST_FORWARD},
+ * {@link KeyEvent.KEYCODE_MEDIA_CLOSE},
+ * {@link KeyEvent.KEYCODE_MEDIA_EJECT},
+ * or {@link KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK}.
+ */
+ public int sendMediaKeyEvent(KeyEvent keyEvent) {
+ if (!MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode())) {
+ Log.e(TAG, "Cannot use sendMediaKeyEvent() for a non-media key event");
+ return ERROR_BAD_VALUE;
+ }
+ boolean registered = false;
+ final PendingIntent pi;
+ synchronized(mInfoLock) {
+ registered = mIsRegistered;
+ pi = mClientPendingIntentCurrent;
+ }
+ if (registered) {
+ if (pi != null) {
+ Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
+ try {
+ pi.send(mContext, 0, intent);
+ } catch (CanceledException e) {
+ Log.e(TAG, "Error sending intent for media button down: ", e);
+ return ERROR;
+ }
+ } else {
+ Log.i(TAG, "No-op when sending key click, no receiver right now");
+ return ERROR;
+ }
+ } else {
+ mAudioManager.dispatchMediaKeyEvent(keyEvent);
+ }
+ return SUCCESS;
+ }
+
+
+ // Error codes
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * Successful operation.
+ */
+ public static final int SUCCESS = 0;
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * Unspecified error.
+ */
+ public static final int ERROR = -1;
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * Operation failed due to bad parameter value.
+ */
+ public static final int ERROR_BAD_VALUE = -2;
+
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * @param timeMs
+ * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE}
+ */
+ public int seekTo(long timeMs) {
+ if (timeMs < 0) {
+ return ERROR_BAD_VALUE;
+ }
+ final int genId;
+ synchronized (mGenLock) {
+ genId = mClientGenerationIdCurrent;
+ }
+ mAudioManager.setRemoteControlClientPlaybackPosition(genId, timeMs);
+ return SUCCESS;
+ }
+
+
+ /**
+ * @hide
+ * must be called on a registered RemoteController
+ * @param wantBitmap
+ * @param width
+ * @param height
+ * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE}
+ */
+ public int setBitmapConfiguration(boolean wantBitmap, int width, int height) {
+ synchronized (mInfoLock) {
+ if (!mIsRegistered) {
+ Log.e(TAG, "Cannot specify bitmap configuration on unregistered RemoteController");
+ return ERROR;
+ }
+ }
+ if (wantBitmap) {
+ if ((width > 0) && (height > 0)) {
+ if (width > MAX_BITMAP_DIMENSION) { width = MAX_BITMAP_DIMENSION; }
+ if (height > MAX_BITMAP_DIMENSION) { height = MAX_BITMAP_DIMENSION; }
+ mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd, width, height);
+ } else {
+ Log.e(TAG, "Invalid dimensions");
+ return ERROR_BAD_VALUE;
+ }
+ } else {
+ mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd, -1, -1);
+ }
+ return SUCCESS;
+ }
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * must be called on a registered RemoteController
+ * @param width
+ * @param height
+ * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE}
+ */
+ public int setBitmapConfiguration(int width, int height) {
+ return setBitmapConfiguration(true, width, height);
+ }
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * must be called on a registered RemoteController
+ * @return {@link #SUCCESS}, {@link #ERROR}
+ */
+ public int setBitmapConfigurationNone() {
+ return setBitmapConfiguration(false, -1, -1);
+ }
+
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * Default playback position synchronization mode where the RemoteControlClient is not
+ * asked regularly for its playback position to see if it has drifted from the estimated
+ * position.
+ */
+ public static final int POSITION_SYNCHRONIZATION_NONE = 0;
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * The playback position synchronization mode where the RemoteControlClient instances which
+ * expose their playback position to the framework, will be regularly polled to check
+ * whether any drift has been noticed between their estimated position and the one they report.
+ * Note that this mode should only ever be used when needing to display very accurate playback
+ * position, as regularly polling a RemoteControlClient for its position may have an impact
+ * on battery life (if applicable) when this query will trigger network transactions in the
+ * case of remote playback.
+ */
+ public static final int POSITION_SYNCHRONIZATION_CHECK = 1;
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * Set the playback position synchronization mode.
+ * Must be called on a registered RemoteController.
+ * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK}
+ * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE}
+ */
+ public int setSynchronizationMode(int sync) {
+ if ((sync != POSITION_SYNCHRONIZATION_NONE) || (sync != POSITION_SYNCHRONIZATION_CHECK)) {
+ Log.e(TAG, "Unknown synchronization mode");
+ return ERROR_BAD_VALUE;
+ }
+ if (!mIsRegistered) {
+ Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController");
+ return ERROR;
+ }
+ mAudioManager.remoteControlDisplayWantsPlaybackPositionSync(mRcd,
+ POSITION_SYNCHRONIZATION_CHECK == sync);
+ return SUCCESS;
+ }
+
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of
+ * the current {@link RemoteControlClient}.
+ * @return a new MetadataEditor instance.
+ */
+ public MetadataEditor editMetadata() {
+ MetadataEditor editor = new MetadataEditor();
+ editor.mEditorMetadata = new Bundle();
+ editor.mEditorArtwork = null;
+ editor.mMetadataChanged = true;
+ editor.mArtworkChanged = true;
+ editor.mEditableKeys = 0;
+ return editor;
+ }
+
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * Used to read the metadata published by a {@link RemoteControlClient}, or send a
+ * {@link RemoteControlClient} new values for keys that can be edited.
+ */
+ public class MetadataEditor extends MediaMetadataEditor {
+ /**
+ * @hide
+ */
+ protected MetadataEditor() { }
+
+ /**
+ * @hide
+ */
+ protected MetadataEditor(Bundle metadata, long editableKeys) {
+ mEditorMetadata = metadata;
+ mEditableKeys = editableKeys;
+ mEditorArtwork = null;
+ mMetadataChanged = true;
+ mArtworkChanged = true;
+ mApplied = false;
+ }
+
+ /**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ */
+ public synchronized void apply() {
+ // "applying" a metadata bundle in RemoteController is only for sending edited
+ // key values back to the RemoteControlClient, so here we only care about the only
+ // editable key we support: RATING_KEY_BY_USER
+ if (!mMetadataChanged) {
+ return;
+ }
+ final int genId;
+ synchronized(mGenLock) {
+ genId = mClientGenerationIdCurrent;
+ }
+ synchronized(mInfoLock) {
+ if (mEditorMetadata.containsKey(
+ String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) {
+ Rating rating = (Rating) getObject(
+ MediaMetadataEditor.RATING_KEY_BY_USER, null);
+ mAudioManager.updateRemoteControlClientMetadata(genId,
+ MediaMetadataEditor.RATING_KEY_BY_USER,
+ rating);
+ } else {
+ Log.e(TAG, "no metadata to apply");
+ }
+ // NOT setting mApplied to true as this type of MetadataEditor will be applied
+ // multiple times, whenever the user of a RemoteController needs to change the
+ // metadata (e.g. user changes the rating of a song more than once during playback)
+ mApplied = false;
+ }
+ }
+
+ }
+
+
+ //==================================================
+ // Implementation of IRemoteControlDisplay interface
+ private class RcDisplay extends IRemoteControlDisplay.Stub {
+ /**
+ * @hide
+ */
+ public void setCurrentClientId(int genId, PendingIntent clientMediaIntent,
+ boolean clearing) {
+ boolean isNew = false;
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ mClientGenerationIdCurrent = genId;
+ isNew = true;
+ }
+ }
+ if (clientMediaIntent != null) {
+ sendMsg(mEventHandler, MSG_NEW_PENDING_INTENT, SENDMSG_REPLACE,
+ genId /*arg1*/, 0, clientMediaIntent /*obj*/, 0 /*delay*/);
+ }
+ if (isNew || clearing) {
+ sendMsg(mEventHandler, MSG_CLIENT_RESET, SENDMSG_REPLACE,
+ genId /*arg1*/, clearing ? 1 : 0, null /*obj*/, 0 /*delay*/);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void setPlaybackState(int genId, int state,
+ long stateChangeTimeMs, long currentPosMs, float speed) {
+ if (DEBUG) {
+ Log.d(TAG, "> new playback state: genId="+genId
+ + " state="+ state
+ + " changeTime="+ stateChangeTimeMs
+ + " pos=" + currentPosMs
+ + "ms speed=" + speed);
+ }
+
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ final PlaybackInfo playbackInfo =
+ new PlaybackInfo(state, stateChangeTimeMs, currentPosMs, speed);
+ sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE,
+ genId /*arg1*/, 0, playbackInfo /*obj*/, 0 /*delay*/);
+
+ }
+
+ /**
+ * @hide
+ */
+ public void setTransportControlInfo(int genId, int transportControlFlags,
+ int posCapabilities) {
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE,
+ genId /*arg1*/, transportControlFlags /*arg2*/,
+ null /*obj*/, 0 /*delay*/);
+ }
+
+ /**
+ * @hide
+ */
+ public void setMetadata(int genId, Bundle metadata) {
+ if (DEBUG) { Log.e(TAG, "setMetadata("+genId+")"); }
+ if (metadata == null) {
+ return;
+ }
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
+ genId /*arg1*/, 0 /*arg2*/,
+ metadata /*obj*/, 0 /*delay*/);
+ }
+
+ /**
+ * @hide
+ */
+ public void setArtwork(int genId, Bitmap artwork) {
+ if (DEBUG) { Log.v(TAG, "setArtwork("+genId+")"); }
+ if (artwork == null) {
+ return;
+ }
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ Bundle metadata = new Bundle(1);
+ metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), artwork);
+ sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
+ genId /*arg1*/, 0 /*arg2*/,
+ metadata /*obj*/, 0 /*delay*/);
+ }
+
+ /**
+ * @hide
+ */
+ public void setAllMetadata(int genId, Bundle metadata, Bitmap artwork) {
+ if (DEBUG) { Log.e(TAG, "setAllMetadata("+genId+")"); }
+ if ((metadata == null) && (artwork == null)) {
+ return;
+ }
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ if (metadata == null) {
+ metadata = new Bundle(1);
+ }
+ if (artwork != null) {
+ metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK),
+ artwork);
+ }
+ sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
+ genId /*arg1*/, 0 /*arg2*/,
+ metadata /*obj*/, 0 /*delay*/);
+ }
+ }
+
+ //==================================================
+ // Event handling
+ private EventHandler mEventHandler;
+ private final static int MSG_NEW_PENDING_INTENT = 0;
+ private final static int MSG_NEW_PLAYBACK_INFO = 1;
+ private final static int MSG_NEW_TRANSPORT_INFO = 2;
+ private final static int MSG_NEW_METADATA = 3; // msg always has non-null obj parameter
+ private final static int MSG_CLIENT_RESET = 4;
+
+ private class EventHandler extends Handler {
+
+ public EventHandler(RemoteController rc, Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_NEW_PENDING_INTENT:
+ onNewPendingIntent(msg.arg1, (PendingIntent) msg.obj);
+ break;
+ case MSG_NEW_PLAYBACK_INFO:
+ onNewPlaybackInfo(msg.arg1, (PlaybackInfo) msg.obj);
+ break;
+ case MSG_NEW_TRANSPORT_INFO:
+ onNewTransportInfo(msg.arg1, msg.arg2);
+ break;
+ case MSG_NEW_METADATA:
+ onNewMetadata(msg.arg1, (Bundle)msg.obj);
+ break;
+ case MSG_CLIENT_RESET:
+ onClientReset(msg.arg1, msg.arg2 == 1);
+ break;
+ default:
+ Log.e(TAG, "unknown event " + msg.what);
+ }
+ }
+ }
+
+ /** If the msg is already queued, replace it with this one. */
+ private static final int SENDMSG_REPLACE = 0;
+ /** If the msg is already queued, ignore this one and leave the old. */
+ private static final int SENDMSG_NOOP = 1;
+ /** If the msg is already queued, queue this one and leave the old. */
+ private static final int SENDMSG_QUEUE = 2;
+
+ private static void sendMsg(Handler handler, int msg, int existingMsgPolicy,
+ int arg1, int arg2, Object obj, int delayMs) {
+ if (handler == null) {
+ Log.e(TAG, "null event handler, will not deliver message " + msg);
+ return;
+ }
+ if (existingMsgPolicy == SENDMSG_REPLACE) {
+ handler.removeMessages(msg);
+ } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
+ return;
+ }
+ handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs);
+ }
+
+ private void onNewPendingIntent(int genId, PendingIntent pi) {
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ synchronized(mInfoLock) {
+ mClientPendingIntentCurrent = pi;
+ }
+ }
+
+ private void onNewPlaybackInfo(int genId, PlaybackInfo pi) {
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ final OnClientUpdateListener l;
+ synchronized(mInfoLock) {
+ l = this.mOnClientUpdateListener;
+ mLastPlaybackInfo = pi;
+ }
+ if (l != null) {
+ if (pi.mCurrentPosMs == RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN) {
+ l.onClientPlaybackStateUpdate(pi.mState);
+ } else {
+ l.onClientPlaybackStateUpdate(pi.mState, pi.mStateChangeTimeMs, pi.mCurrentPosMs,
+ pi.mSpeed);
+ }
+ }
+ }
+
+ private void onNewTransportInfo(int genId, int transportControlFlags) {
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ final OnClientUpdateListener l;
+ synchronized(mInfoLock) {
+ l = mOnClientUpdateListener;
+ mLastTransportControlFlags = transportControlFlags;
+ }
+ if (l != null) {
+ l.onClientTransportControlUpdate(transportControlFlags);
+ }
+ }
+
+ /**
+ * @param genId
+ * @param metadata guaranteed to be always non-null
+ */
+ private void onNewMetadata(int genId, Bundle metadata) {
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ final OnClientUpdateListener l;
+ final MetadataEditor metadataEditor;
+ // prepare the received Bundle to be used inside a MetadataEditor
+ final long editableKeys = metadata.getLong(
+ String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK), 0);
+ if (editableKeys != 0) {
+ metadata.remove(String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK));
+ }
+ synchronized(mInfoLock) {
+ l = mOnClientUpdateListener;
+ if ((mMetadataEditor != null) && (mMetadataEditor.mEditorMetadata != null)) {
+ if (mMetadataEditor.mEditorMetadata != metadata) {
+ // existing metadata, merge existing and new
+ mMetadataEditor.mEditorMetadata.putAll(metadata);
+ }
+ } else {
+ mMetadataEditor = new MetadataEditor(metadata, editableKeys);
+ }
+ metadataEditor = mMetadataEditor;
+ }
+ if (l != null) {
+ l.onClientMetadataUpdate(metadataEditor);
+ }
+ }
+
+ private void onClientReset(int genId, boolean clearing) {
+ synchronized(mGenLock) {
+ if (mClientGenerationIdCurrent != genId) {
+ return;
+ }
+ }
+ final OnClientUpdateListener l;
+ synchronized(mInfoLock) {
+ l = mOnClientUpdateListener;
+ }
+ if (l != null) {
+ l.onClientReset(clearing);
+ }
+ }
+
+
+ //==================================================
+ private static class PlaybackInfo {
+ int mState;
+ long mStateChangeTimeMs;
+ long mCurrentPosMs;
+ float mSpeed;
+
+ PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) {
+ mState = state;
+ mStateChangeTimeMs = stateChangeTimeMs;
+ mCurrentPosMs = currentPosMs;
+ mSpeed = speed;
+ }
+ }
+
+ /**
+ * @hide
+ * Used by AudioManager to mark this instance as registered.
+ * @param registered
+ */
+ protected void setIsRegistered(boolean registered) {
+ synchronized (mInfoLock) {
+ mIsRegistered = registered;
+ }
+ }
+
+ /**
+ * @hide
+ * Used by AudioManager to access binder to be registered/unregistered inside MediaFocusControl
+ * @return
+ */
+ protected RcDisplay getRcDisplay() {
+ return mRcd;
+ }
+}
diff --git a/media/java/android/media/SubtitleController.java b/media/java/android/media/SubtitleController.java
index e83c5ba..8090561 100644
--- a/media/java/android/media/SubtitleController.java
+++ b/media/java/android/media/SubtitleController.java
@@ -38,6 +38,21 @@ public class SubtitleController {
private boolean mShowing;
private CaptioningManager mCaptioningManager;
+ private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener =
+ new CaptioningManager.CaptioningChangeListener() {
+ /** @hide */
+ @Override
+ public void onEnabledChanged(boolean enabled) {
+ selectDefaultTrack();
+ }
+
+ /** @hide */
+ @Override
+ public void onLocaleChanged(Locale locale) {
+ selectDefaultTrack();
+ }
+ };
+
/**
* Creates a subtitle controller for a media playback object that implements
* the MediaTimeProvider interface.
@@ -58,15 +73,24 @@ public class SubtitleController {
(CaptioningManager)context.getSystemService(Context.CAPTIONING_SERVICE);
}
+ @Override
+ protected void finalize() throws Throwable {
+ mCaptioningManager.removeCaptioningChangeListener(
+ mCaptioningChangeListener);
+ super.finalize();
+ }
+
/**
* @return the available subtitle tracks for this media. These include
* the tracks found by {@link MediaPlayer} as well as any tracks added
* manually via {@link #addTrack}.
*/
public SubtitleTrack[] getTracks() {
- SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
- mTracks.toArray(tracks);
- return tracks;
+ synchronized(mTracks) {
+ SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
+ mTracks.toArray(tracks);
+ return tracks;
+ }
}
/**
@@ -88,6 +112,8 @@ public class SubtitleController {
* in-band data from the {@link MediaPlayer}. However, this does
* not change the subtitle visibility.
*
+ * Must be called from the UI thread.
+ *
* @param track The subtitle track to select. This must be one of the
* tracks in {@link #getTracks}.
* @return true if the track was successfully selected.
@@ -107,7 +133,9 @@ public class SubtitleController {
}
mSelectedTrack = track;
- mAnchor.setSubtitleWidget(getRenderingWidget());
+ if (mAnchor != null) {
+ mAnchor.setSubtitleWidget(getRenderingWidget());
+ }
if (mSelectedTrack != null) {
mSelectedTrack.setTimeProvider(mTimeProvider);
@@ -123,56 +151,123 @@ public class SubtitleController {
/**
* @return the default subtitle track based on system preferences, or null,
* if no such track exists in this manager.
+ *
+ * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT.
+ *
+ * 1. If captioning is disabled, only consider FORCED tracks. Otherwise,
+ * consider all tracks, but prefer non-FORCED ones.
+ * 2. If user selected "Default" caption language:
+ * a. If there is a considered track with DEFAULT=yes, returns that track
+ * (favor the first one in the current language if there are more than
+ * one default tracks, or the first in general if none of them are in
+ * the current language).
+ * b. Otherwise, if there is a track with AUTOSELECT=yes in the current
+ * language, return that one.
+ * c. If there are no default tracks, and no autoselectable tracks in the
+ * current language, return null.
+ * 3. If there is a track with the caption language, select that one. Prefer
+ * the one with AUTOSELECT=no.
+ *
+ * The default values for these flags are DEFAULT=no, AUTOSELECT=yes
+ * and FORCED=no.
+ *
+ * Must be called from the UI thread.
*/
public SubtitleTrack getDefaultTrack() {
- Locale locale = mCaptioningManager.getLocale();
-
- for (SubtitleTrack track: mTracks) {
- MediaFormat format = track.getFormat();
- String language = format.getString(MediaFormat.KEY_LANGUAGE);
- // TODO: select track with best renderer. For now, we select first
- // track with local's language or first track if locale has none
- if (locale == null ||
- locale.getLanguage().equals("") ||
- locale.getISO3Language().equals(language) ||
- locale.getLanguage().equals(language)) {
- return track;
+ SubtitleTrack bestTrack = null;
+ int bestScore = -1;
+
+ Locale selectedLocale = mCaptioningManager.getLocale();
+ Locale locale = selectedLocale;
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+ boolean selectForced = !mCaptioningManager.isEnabled();
+
+ synchronized(mTracks) {
+ for (SubtitleTrack track: mTracks) {
+ MediaFormat format = track.getFormat();
+ String language = format.getString(MediaFormat.KEY_LANGUAGE);
+ boolean forced =
+ format.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0;
+ boolean autoselect =
+ format.getInteger(MediaFormat.KEY_IS_AUTOSELECT, 1) != 0;
+ boolean is_default =
+ format.getInteger(MediaFormat.KEY_IS_DEFAULT, 0) != 0;
+
+ boolean languageMatches =
+ (locale == null ||
+ locale.getLanguage().equals("") ||
+ locale.getISO3Language().equals(language) ||
+ locale.getLanguage().equals(language));
+ // is_default is meaningless unless caption language is 'default'
+ int score = (forced ? 0 : 8) +
+ (((selectedLocale == null) && is_default) ? 4 : 0) +
+ (autoselect ? 0 : 2) + (languageMatches ? 1 : 0);
+
+ if (selectForced && !forced) {
+ continue;
+ }
+
+ // we treat null locale/language as matching any language
+ if ((selectedLocale == null && is_default) ||
+ (languageMatches &&
+ (autoselect || forced || selectedLocale != null))) {
+ if (score > bestScore) {
+ bestScore = score;
+ bestTrack = track;
+ }
+ }
}
}
- return null;
+ return bestTrack;
}
private boolean mTrackIsExplicit = false;
private boolean mVisibilityIsExplicit = false;
- /** @hide */
+ /** @hide - called from UI thread */
public void selectDefaultTrack() {
if (mTrackIsExplicit) {
+ // If track selection is explicit, but visibility
+ // is not, it falls back to the captioning setting
+ if (!mVisibilityIsExplicit) {
+ if (mCaptioningManager.isEnabled() ||
+ (mSelectedTrack != null &&
+ mSelectedTrack.getFormat().getInteger(
+ MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) {
+ show();
+ } else {
+ hide();
+ }
+ mVisibilityIsExplicit = false;
+ }
return;
}
+ // We can have a default (forced) track even if captioning
+ // is not enabled. This is handled by getDefaultTrack().
+ // Show this track unless subtitles were explicitly hidden.
SubtitleTrack track = getDefaultTrack();
if (track != null) {
selectTrack(track);
mTrackIsExplicit = false;
if (!mVisibilityIsExplicit) {
- if (mCaptioningManager.isEnabled()) {
- show();
- } else {
- hide();
- }
+ show();
mVisibilityIsExplicit = false;
}
}
}
- /** @hide */
+ /** @hide - called from UI thread */
public void reset() {
hide();
selectTrack(null);
mTracks.clear();
mTrackIsExplicit = false;
mVisibilityIsExplicit = false;
+ mCaptioningManager.removeCaptioningChangeListener(
+ mCaptioningChangeListener);
}
/**
@@ -183,12 +278,20 @@ public class SubtitleController {
* @return the created {@link SubtitleTrack} object
*/
public SubtitleTrack addTrack(MediaFormat format) {
- for (Renderer renderer: mRenderers) {
- if (renderer.supports(format)) {
- SubtitleTrack track = renderer.createTrack(format);
- if (track != null) {
- mTracks.add(track);
- return track;
+ synchronized(mRenderers) {
+ for (Renderer renderer: mRenderers) {
+ if (renderer.supports(format)) {
+ SubtitleTrack track = renderer.createTrack(format);
+ if (track != null) {
+ synchronized(mTracks) {
+ if (mTracks.size() == 0) {
+ mCaptioningManager.addCaptioningChangeListener(
+ mCaptioningChangeListener);
+ }
+ mTracks.add(track);
+ }
+ return track;
+ }
}
}
}
@@ -197,6 +300,8 @@ public class SubtitleController {
/**
* Show the selected (or default) subtitle track.
+ *
+ * Must be called from the UI thread.
*/
public void show() {
mShowing = true;
@@ -208,6 +313,8 @@ public class SubtitleController {
/**
* Hide the selected (or default) subtitle track.
+ *
+ * Must be called from the UI thread.
*/
public void hide() {
mVisibilityIsExplicit = true;
@@ -257,10 +364,12 @@ public class SubtitleController {
* support for a subtitle format.
*/
public void registerRenderer(Renderer renderer) {
- // TODO how to get available renderers in the system
- if (!mRenderers.contains(renderer)) {
- // TODO should added renderers override existing ones (to allow replacing?)
- mRenderers.add(renderer);
+ synchronized(mRenderers) {
+ // TODO how to get available renderers in the system
+ if (!mRenderers.contains(renderer)) {
+ // TODO should added renderers override existing ones (to allow replacing?)
+ mRenderers.add(renderer);
+ }
}
}
@@ -279,7 +388,7 @@ public class SubtitleController {
private Anchor mAnchor;
- /** @hide */
+ /** @hide - called from UI thread */
public void setAnchor(Anchor anchor) {
if (mAnchor == anchor) {
return;
diff --git a/media/java/android/media/SubtitleTrack.java b/media/java/android/media/SubtitleTrack.java
index cb689af..06063de 100644
--- a/media/java/android/media/SubtitleTrack.java
+++ b/media/java/android/media/SubtitleTrack.java
@@ -69,7 +69,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList
}
/** @hide */
- public MediaFormat getFormat() {
+ public final MediaFormat getFormat() {
return mFormat;
}
@@ -201,7 +201,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList
}
/** @hide */
- public void scheduleTimedEvents() {
+ protected void scheduleTimedEvents() {
/* get times for the next event */
if (mTimeProvider != null) {
mNextScheduledTimeMs = mCues.nextTimeAfter(mLastTimeMs);
@@ -363,7 +363,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList
}
/** @hide */
- public void setTimeProvider(MediaTimeProvider timeProvider) {
+ public synchronized void setTimeProvider(MediaTimeProvider timeProvider) {
if (mTimeProvider == timeProvider) {
return;
}