diff options
Diffstat (limited to 'media')
-rw-r--r-- | media/java/android/media/AudioManager.java | 84 | ||||
-rw-r--r-- | media/java/android/media/AudioService.java | 104 | ||||
-rw-r--r-- | media/java/android/media/IAudioService.aidl | 10 | ||||
-rw-r--r-- | media/java/android/media/ImageReader.java | 68 | ||||
-rw-r--r-- | media/java/android/media/MediaFocusControl.java | 43 | ||||
-rw-r--r-- | media/java/android/media/MediaFormat.java | 36 | ||||
-rw-r--r-- | media/java/android/media/MediaMetadataEditor.java | 2 | ||||
-rw-r--r-- | media/java/android/media/MediaPlayer.java | 49 | ||||
-rw-r--r-- | media/java/android/media/Rating.java | 10 | ||||
-rw-r--r-- | media/java/android/media/RemoteControlClient.java | 6 | ||||
-rw-r--r-- | media/java/android/media/RemoteController.java | 796 | ||||
-rw-r--r-- | media/java/android/media/SubtitleController.java | 179 | ||||
-rw-r--r-- | media/java/android/media/SubtitleTrack.java | 6 |
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; } |