diff options
Diffstat (limited to 'media/java')
34 files changed, 2855 insertions, 1828 deletions
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 5a3aaab..394e437 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -17,6 +17,8 @@ package android.media; import android.annotation.IntDef; +import android.os.Parcel; +import android.os.Parcelable; import android.util.Log; import java.lang.annotation.Retention; @@ -26,11 +28,10 @@ import java.util.HashSet; import java.util.Set; /** - * @hide * A class to encapsulate a collection of attributes describing information about an audio * player or recorder. */ -public final class AudioAttributes { +public final class AudioAttributes implements Parcelable { private final static String TAG = "AudioAttributes"; /** @@ -193,6 +194,7 @@ public final class AudioAttributes { } /** + * @hide * Return the set of tags. * @return a read-only set of all tags stored as strings. */ @@ -325,6 +327,7 @@ public final class AudioAttributes { } /** + * @hide * Add a custom tag stored as a string * @param tag * @return the same Builder instance. @@ -411,6 +414,49 @@ public final class AudioAttributes { + " tags=" + mTags); } + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mUsage); + dest.writeInt(mContentType); + dest.writeInt(mFlags); + String[] tagsArray = new String[mTags.size()]; + mTags.toArray(tagsArray); + dest.writeStringArray(tagsArray); + } + + private AudioAttributes(Parcel in) { + mUsage = in.readInt(); + mContentType = in.readInt(); + mFlags = in.readInt(); + mTags = new HashSet<String>(); + String[] tagsArray = in.readStringArray(); + for (int i = tagsArray.length - 1 ; i >= 0 ; i--) { + mTags.add(tagsArray[i]); + } + } + + /** @hide */ + public static final Parcelable.Creator<AudioAttributes> CREATOR + = new Parcelable.Creator<AudioAttributes>() { + /** + * Rebuilds an AudioAttributes previously stored with writeToParcel(). + * @param p Parcel object to read the AudioAttributes from + * @return a new AudioAttributes created from the data in the parcel + */ + public AudioAttributes createFromParcel(Parcel p) { + return new AudioAttributes(p); + } + public AudioAttributes[] newArray(int size) { + return new AudioAttributes[size]; + } + }; + + /** @hide */ @IntDef({ USAGE_UNKNOWN, diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index 57274ee..bd2be1b 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -16,14 +16,19 @@ package android.media; +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * The AudioFormat class is used to access a number of audio format and * channel configuration constants. They are for instance used * in {@link AudioTrack} and {@link AudioRecord}. - * + * */ public class AudioFormat { - + //--------------------------------------------------------- // Constants //-------------------- @@ -39,6 +44,10 @@ public class AudioFormat { public static final int ENCODING_PCM_8BIT = 3; /** Audio data format: single-precision floating-point per sample */ public static final int ENCODING_PCM_FLOAT = 4; + /** Audio data format: AC-3 compressed */ + public static final int ENCODING_AC3 = 5; + /** Audio data format: E-AC-3 compressed */ + public static final int ENCODING_E_AC3 = 6; /** Invalid audio channel configuration */ /** @deprecated use CHANNEL_INVALID instead */ @@ -150,10 +159,204 @@ public class AudioFormat { case ENCODING_PCM_16BIT: case ENCODING_DEFAULT: return 2; + case ENCODING_PCM_FLOAT: + return 4; case ENCODING_INVALID: default: throw new IllegalArgumentException("Bad audio format " + audioFormat); } } + /** @hide */ + public static boolean isValidEncoding(int audioFormat) + { + switch (audioFormat) { + case ENCODING_PCM_8BIT: + case ENCODING_PCM_16BIT: + case ENCODING_PCM_FLOAT: + case ENCODING_AC3: + case ENCODING_E_AC3: + return true; + default: + return false; + } + } + + /** @hide */ + public static boolean isEncodingLinearPcm(int audioFormat) + { + switch (audioFormat) { + case ENCODING_PCM_8BIT: + case ENCODING_PCM_16BIT: + case ENCODING_PCM_FLOAT: + case ENCODING_DEFAULT: + return true; + case ENCODING_AC3: + case ENCODING_E_AC3: + return false; + case ENCODING_INVALID: + default: + throw new IllegalArgumentException("Bad audio format " + audioFormat); + } + } + + /** @removed */ + public AudioFormat() + { + throw new UnsupportedOperationException("There is no valid usage of this constructor"); + } + + /** + * Private constructor with an ignored argument to differentiate from the removed default ctor + * @param ignoredArgument + */ + private AudioFormat(int ignoredArgument) { + } + + /** @hide */ + public final static int AUDIO_FORMAT_HAS_PROPERTY_NONE = 0x0; + /** @hide */ + public final static int AUDIO_FORMAT_HAS_PROPERTY_ENCODING = 0x1 << 0; + /** @hide */ + public final static int AUDIO_FORMAT_HAS_PROPERTY_SAMPLE_RATE = 0x1 << 1; + /** @hide */ + public final static int AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK = 0x1 << 2; + + private int mEncoding; + private int mSampleRate; + private int mChannelMask; + private int mPropertySetMask; + + /** + * @hide CANDIDATE FOR PUBLIC API + * Builder class for {@link AudioFormat} objects. + */ + public static class Builder { + private int mEncoding = ENCODING_DEFAULT; + private int mSampleRate = 0; + private int mChannelMask = CHANNEL_INVALID; + private int mPropertySetMask = AUDIO_FORMAT_HAS_PROPERTY_NONE; + + /** + * Constructs a new Builder with the defaults. + */ + public Builder() { + } + + /** + * Constructs a new Builder from a given {@link AudioFormat}. + * @param af the {@link AudioFormat} object whose data will be reused in the new Builder. + */ + public Builder(AudioFormat af) { + mEncoding = af.mEncoding; + mSampleRate = af.mSampleRate; + mChannelMask = af.mChannelMask; + mPropertySetMask = af.mPropertySetMask; + } + + /** + * Combines all of the format characteristics that have been set and return a new + * {@link AudioFormat} object. + * @return a new {@link AudioFormat} object + */ + public AudioFormat build() { + AudioFormat af = new AudioFormat(1980/*ignored*/); + af.mEncoding = mEncoding; + af.mSampleRate = mSampleRate; + af.mChannelMask = mChannelMask; + af.mPropertySetMask = mPropertySetMask; + return af; + } + + /** + * Sets the data encoding format. + * @param encoding one of {@link AudioFormat#ENCODING_DEFAULT}, + * {@link AudioFormat#ENCODING_PCM_8BIT}, + * {@link AudioFormat#ENCODING_PCM_16BIT}, + * {@link AudioFormat#ENCODING_PCM_FLOAT}, + * {@link AudioFormat#ENCODING_AC3}, + * {@link AudioFormat#ENCODING_E_AC3}. + * @return the same Builder instance. + * @throws java.lang.IllegalArgumentException + */ + public Builder setEncoding(@Encoding int encoding) throws IllegalArgumentException { + switch (encoding) { + case ENCODING_DEFAULT: + mEncoding = ENCODING_PCM_16BIT; + break; + case ENCODING_PCM_8BIT: + case ENCODING_PCM_16BIT: + case ENCODING_PCM_FLOAT: + case ENCODING_AC3: + case ENCODING_E_AC3: + mEncoding = encoding; + break; + case ENCODING_INVALID: + default: + throw new IllegalArgumentException("Invalid encoding " + encoding); + } + mPropertySetMask |= AUDIO_FORMAT_HAS_PROPERTY_ENCODING; + return this; + } + + /** + * Sets the channel mask. + * @param channelMask describes the configuration of the audio channels. + * <p>For output, the mask should be a combination of + * {@link AudioFormat#CHANNEL_OUT_FRONT_LEFT}, + * {@link AudioFormat#CHANNEL_OUT_FRONT_CENTER}, + * {@link AudioFormat#CHANNEL_OUT_FRONT_RIGHT}, + * {@link AudioFormat#CHANNEL_OUT_SIDE_LEFT}, + * {@link AudioFormat#CHANNEL_OUT_SIDE_RIGHT}, + * {@link AudioFormat#CHANNEL_OUT_BACK_LEFT}, + * {@link AudioFormat#CHANNEL_OUT_BACK_RIGHT}. + * <p>for input, the mask should be {@link AudioFormat#CHANNEL_IN_MONO} or + * {@link AudioFormat#CHANNEL_IN_STEREO}. {@link AudioFormat#CHANNEL_IN_MONO} is + * guaranteed to work on all devices. + * @return the same Builder instance. + */ + public Builder setChannelMask(int channelMask) { + // only validated when used, with input or output context + mChannelMask = channelMask; + mPropertySetMask |= AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK; + return this; + } + + /** + * Sets the sample rate. + * @param sampleRate the sample rate expressed in Hz + * @return the same Builder instance. + * @throws java.lang.IllegalArgumentException + */ + public Builder setSampleRate(int sampleRate) throws IllegalArgumentException { + if ((sampleRate <= 0) || (sampleRate > 192000)) { + throw new IllegalArgumentException("Invalid sample rate " + sampleRate); + } + mSampleRate = sampleRate; + mPropertySetMask |= AUDIO_FORMAT_HAS_PROPERTY_SAMPLE_RATE; + return this; + } + } + + @Override + public String toString () { + return new String("AudioFormat:" + + " props=" + mPropertySetMask + + " enc=" + mEncoding + + " chan=0x" + Integer.toHexString(mChannelMask) + + " rate=" + mSampleRate); + } + + /** @hide */ + @IntDef({ + ENCODING_DEFAULT, + ENCODING_PCM_8BIT, + ENCODING_PCM_16BIT, + ENCODING_PCM_FLOAT, + ENCODING_AC3, + ENCODING_E_AC3 + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Encoding {} + } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 458139b..fb19242 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -50,12 +50,6 @@ import java.util.ArrayList; */ public class AudioManager { - // If we should use the new sessions APIs. - private final static boolean USE_SESSIONS = true; - // If we should use the legacy APIs. If both are true information will be - // duplicated through both paths. Currently this flag isn't used. - private final static boolean USE_LEGACY = true; - private final Context mContext; private long mVolumeKeyUpTime; private final boolean mUseMasterVolume; @@ -483,17 +477,8 @@ public class AudioManager { * or {@link KeyEvent#KEYCODE_MEDIA_AUDIO_TRACK}. */ public void dispatchMediaKeyEvent(KeyEvent keyEvent) { - if (USE_SESSIONS) { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); - helper.sendMediaButtonEvent(keyEvent, false); - } else { - IAudioService service = getService(); - try { - service.dispatchMediaKeyEvent(keyEvent); - } catch (RemoteException e) { - Log.e(TAG, "dispatchMediaKeyEvent threw exception ", e); - } - } + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + helper.sendMediaButtonEvent(keyEvent, false); } /** @@ -644,12 +629,8 @@ public class AudioManager { if (mUseMasterVolume) { service.adjustMasterVolume(direction, flags, mContext.getOpPackageName()); } else { - if (USE_SESSIONS) { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); - helper.sendAdjustVolumeBy(USE_DEFAULT_STREAM_TYPE, direction, flags); - } else { - service.adjustVolume(direction, flags, mContext.getOpPackageName()); - } + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + helper.sendAdjustVolumeBy(USE_DEFAULT_STREAM_TYPE, direction, flags); } } catch (RemoteException e) { Log.e(TAG, "Dead object in adjustVolume", e); @@ -679,13 +660,8 @@ public class AudioManager { if (mUseMasterVolume) { service.adjustMasterVolume(direction, flags, mContext.getOpPackageName()); } else { - if (USE_SESSIONS) { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); - helper.sendAdjustVolumeBy(suggestedStreamType, direction, flags); - } else { - service.adjustSuggestedStreamVolume(direction, suggestedStreamType, flags, - mContext.getOpPackageName()); - } + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + helper.sendAdjustVolumeBy(suggestedStreamType, direction, flags); } } catch (RemoteException e) { Log.e(TAG, "Dead object in adjustSuggestedStreamVolume", e); @@ -1017,7 +993,7 @@ public class AudioManager { public void setMasterMute(boolean state, int flags) { IAudioService service = getService(); try { - service.setMasterMute(state, flags, mICallBack); + service.setMasterMute(state, flags, mContext.getOpPackageName(), mICallBack); } catch (RemoteException e) { Log.e(TAG, "Dead object in setMasterMute", e); } @@ -1278,11 +1254,6 @@ public class AudioManager { * call {@link #stopBluetoothSco()} to clear the request and turn down the bluetooth connection. * <p>Even if a SCO connection is established, the following restrictions apply on audio * output streams so that they can be routed to SCO headset: - * <p>NOTE: up to and including API version - * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}, this method initiates a virtual - * voice call to the bluetooth headset. - * After API version {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} only a raw SCO audio - * connection is established. * <ul> * <li> the stream type must be {@link #STREAM_VOICE_CALL} </li> * <li> the format must be mono </li> @@ -1298,6 +1269,11 @@ public class AudioManager { * it will be ignored. Similarly, if a call is received or sent while an application * is using the SCO connection, the connection will be lost for the application and NOT * returned automatically when the call ends. + * <p>NOTE: up to and including API version + * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}, this method initiates a virtual + * voice call to the bluetooth headset. + * After API version {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} only a raw SCO audio + * connection is established. * @see #stopBluetoothSco() * @see #ACTION_SCO_AUDIO_STATE_UPDATED */ @@ -1311,13 +1287,38 @@ public class AudioManager { } /** + * Start bluetooth SCO audio connection in virtual call mode. + * <p>Requires Permission: + * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS}. + * <p>Similar to {@link #startBluetoothSco()} with explicit selection of virtual call mode. + * Telephony and communication applications (VoIP, Video Chat) should preferably select + * virtual call mode. + * Applications using voice input for search or commands should first try raw audio connection + * with {@link #startBluetoothSco()} and fall back to startBluetoothScoVirtualCall() in case of + * failure. + * @see #startBluetoothSco() + * @see #stopBluetoothSco() + * @see #ACTION_SCO_AUDIO_STATE_UPDATED + */ + public void startBluetoothScoVirtualCall() { + IAudioService service = getService(); + try { + service.startBluetoothScoVirtualCall(mICallBack); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in startBluetoothScoVirtualCall", e); + } + } + + /** * Stop bluetooth SCO audio connection. * <p>Requires Permission: * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS}. * <p>This method must be called by applications having requested the use of - * bluetooth SCO audio with {@link #startBluetoothSco()} - * when finished with the SCO connection or if connection fails. + * bluetooth SCO audio with {@link #startBluetoothSco()} or + * {@link #startBluetoothScoVirtualCall()} when finished with the SCO connection or + * if connection fails. * @see #startBluetoothSco() + * @see #startBluetoothScoVirtualCall() */ public void stopBluetoothSco(){ IAudioService service = getService(); @@ -1425,7 +1426,12 @@ public class AudioManager { * <var>false</var> to turn mute off */ public void setMicrophoneMute(boolean on){ - AudioSystem.muteMicrophone(on); + IAudioService service = getService(); + try { + service.setMicrophoneMute(on, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setMicrophoneMute", e); + } } /** @@ -1635,24 +1641,23 @@ public class AudioManager { } /** - * @hide - * If the stream is active locally or remotely, adjust its volume according to the enforced - * priority rules. - * Note: only AudioManager.STREAM_MUSIC is supported at the moment + * Return a new audio session identifier not associated with any player or effect. + * It can for instance be used to create one of the {@link android.media.audiofx.AudioEffect} + * objects. + * @return a new unclaimed and unused audio session identifier, or {@link #ERROR} when the + * system failed to allocate a new session. */ - public void adjustLocalOrRemoteStreamVolume(int streamType, int direction) { - if (streamType != STREAM_MUSIC) { - Log.w(TAG, "adjustLocalOrRemoteStreamVolume() doesn't support stream " + streamType); - } - IAudioService service = getService(); - try { - service.adjustLocalOrRemoteStreamVolume(streamType, direction, - mContext.getOpPackageName()); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in adjustLocalOrRemoteStreamVolume", e); + public int allocateAudioSessionId() { + int session = AudioSystem.newAudioSessionId(); + if (session > 0) { + return session; + } else { + Log.e(TAG, "Failure to allocate a new audio session ID"); + return ERROR; } } + /* * Sets a generic audio configuration parameter. The use of these parameters * are platform dependant, see libaudio @@ -2189,49 +2194,8 @@ public class AudioManager { Log.e(TAG, "Cannot call registerMediaButtonIntent() with a null parameter"); return; } - IAudioService service = getService(); - try { - // pi != null - service.registerMediaButtonIntent(pi, eventReceiver, - eventReceiver == null ? mToken : null); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in registerMediaButtonIntent"+e); - } - if (USE_SESSIONS) { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); - helper.addMediaButtonListener(pi, mContext); - } - } - - /** - * @hide - * Used internally by telephony package to register an intent receiver for ACTION_MEDIA_BUTTON. - * @param eventReceiver the component that will receive the media button key events, - * no-op if eventReceiver is null - */ - public void registerMediaButtonEventReceiverForCalls(ComponentName eventReceiver) { - if (eventReceiver == null) { - return; - } - IAudioService service = getService(); - try { - // eventReceiver != null - service.registerMediaButtonEventReceiverForCalls(eventReceiver); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in registerMediaButtonEventReceiverForCalls", e); - } - } - - /** - * @hide - */ - public void unregisterMediaButtonEventReceiverForCalls() { - IAudioService service = getService(); - try { - service.unregisterMediaButtonEventReceiverForCalls(); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in unregisterMediaButtonEventReceiverForCalls", e); - } + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + helper.addMediaButtonListener(pi, eventReceiver, mContext); } /** @@ -2268,16 +2232,8 @@ public class AudioManager { * @hide */ public void unregisterMediaButtonIntent(PendingIntent pi) { - IAudioService service = getService(); - try { - service.unregisterMediaButtonIntent(pi); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in unregisterMediaButtonIntent"+e); - } - if (USE_SESSIONS) { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); - helper.removeMediaButtonListener(pi); - } + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + helper.removeMediaButtonListener(pi); } /** @@ -2291,20 +2247,7 @@ public class AudioManager { if ((rcClient == null) || (rcClient.getRcMediaIntent() == null)) { return; } - IAudioService service = getService(); - try { - int rcseId = service.registerRemoteControlClient( - rcClient.getRcMediaIntent(), /* mediaIntent */ - rcClient.getIRemoteControlClient(),/* rcClient */ - // used to match media button event receiver and audio focus - mContext.getPackageName()); /* packageName */ - rcClient.setRcseId(rcseId); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in registerRemoteControlClient"+e); - } - if (USE_SESSIONS) { - rcClient.registerWithSession(MediaSessionLegacyHelper.getHelper(mContext)); - } + rcClient.registerWithSession(MediaSessionLegacyHelper.getHelper(mContext)); } /** @@ -2317,16 +2260,7 @@ public class AudioManager { if ((rcClient == null) || (rcClient.getRcMediaIntent() == null)) { return; } - IAudioService service = getService(); - try { - service.unregisterRemoteControlClient(rcClient.getRcMediaIntent(), /* mediaIntent */ - rcClient.getIRemoteControlClient()); /* rcClient */ - } catch (RemoteException e) { - Log.e(TAG, "Dead object in unregisterRemoteControlClient"+e); - } - if (USE_SESSIONS) { - rcClient.unregisterWithSession(MediaSessionLegacyHelper.getHelper(mContext)); - } + rcClient.unregisterWithSession(MediaSessionLegacyHelper.getHelper(mContext)); } /** @@ -2344,25 +2278,8 @@ public class AudioManager { if (rctlr == null) { return false; } - if (USE_SESSIONS) { - rctlr.startListeningToSessions(); - return true; - } else { - IAudioService service = getService(); - final RemoteController.OnClientUpdateListener l = rctlr.getUpdateListener(); - final ComponentName listenerComponent = new ComponentName(mContext, l.getClass()); - try { - int[] artworkDimensions = rctlr.getArtworkSize(); - boolean reg = service.registerRemoteController(rctlr.getRcDisplay(), - artworkDimensions[0]/* w */, artworkDimensions[1]/* h */, - listenerComponent); - rctlr.setIsRegistered(reg); - return reg; - } catch (RemoteException e) { - Log.e(TAG, "Dead object in registerRemoteController " + e); - return false; - } - } + rctlr.startListeningToSessions(); + return true; } /** @@ -2374,17 +2291,7 @@ public class AudioManager { if (rctlr == null) { return; } - if (USE_SESSIONS) { - rctlr.stopListeningToSessions(); - } else { - IAudioService service = getService(); - try { - service.unregisterRemoteControlDisplay(rctlr.getRcDisplay()); - rctlr.setIsRegistered(false); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in unregisterRemoteControlDisplay " + e); - } - } + rctlr.stopListeningToSessions(); } /** @@ -2490,46 +2397,6 @@ public class AudioManager { } /** - * @hide - * Request the user of a RemoteControlClient to seek to the given playback position. - * @param generationId the RemoteControlClient generation counter for which this request is - * issued. Requests for an older generation than current one will be ignored. - * @param timeMs the time in ms to seek to, must be positive. - */ - public void setRemoteControlClientPlaybackPosition(int generationId, long timeMs) { - if (timeMs < 0) { - return; - } - IAudioService service = getService(); - try { - service.setRemoteControlClientPlaybackPosition(generationId, timeMs); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in setRccPlaybackPosition("+ generationId + ", " - + timeMs + ")", e); - } - } - - /** - * @hide - * 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, - Rating value) { - IAudioService service = getService(); - try { - service.updateRemoteControlClientMetadata(generationId, key, value); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in updateRemoteControlClientMetadata("+ generationId + ", " - + key +", " + value + ")", e); - } - } - - /** * @hide * Reload audio settings. This method is called by Settings backup * agent when audio settings are restored and causes the AudioService @@ -2586,6 +2453,9 @@ public class AudioManager { // from AudioManager. AudioSystem is an internal class used by AudioManager and AudioService. /** @hide + * The audio device code for representing "no device." */ + public static final int DEVICE_NONE = AudioSystem.DEVICE_NONE; + /** @hide * The audio output device code for the small speaker at the front of the device used * when placing calls. Does not refer to an in-ear headphone without attached microphone, * such as earbuds, earphones, or in-ear monitors (IEM). Those would be handled as a @@ -2846,18 +2716,22 @@ public class AudioManager { } /** - * Indicate A2DP sink connection state change. + * Indicate A2DP source or sink connection state change. * @param device Bluetooth device connected/disconnected * @param state new connection state (BluetoothProfile.STATE_xxx) + * @param profile profile for the A2DP device + * (either {@link android.bluetooth.BluetoothProfile.A2DP} or + * {@link android.bluetooth.BluetoothProfile.A2DP_SINK}) * @return a delay in ms that the caller should wait before broadcasting * BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED intent. * {@hide} */ - public int setBluetoothA2dpDeviceConnectionState(BluetoothDevice device, int state) { + public int setBluetoothA2dpDeviceConnectionState(BluetoothDevice device, int state, + int profile) { IAudioService service = getService(); int delay = 0; try { - delay = service.setBluetoothA2dpDeviceConnectionState(device, state); + delay = service.setBluetoothA2dpDeviceConnectionState(device, state, profile); } catch (RemoteException e) { Log.e(TAG, "Dead object in setBluetoothA2dpDeviceConnectionState "+e); } finally { @@ -2938,12 +2812,9 @@ public class AudioManager { * @hide */ public int getRemoteStreamVolume() { - try { - return getService().getRemoteStreamVolume(); - } catch (RemoteException e) { - Log.w(TAG, "Error getting remote stream volume", e); - return 0; - } + // TODO STOPSHIP switch callers to use media sessions instead + Log.e(TAG, "Need to implement new Remote Volume!"); + return 0; } /** @@ -2951,12 +2822,9 @@ public class AudioManager { * @hide */ public int getRemoteStreamMaxVolume() { - try { - return getService().getRemoteStreamMaxVolume(); - } catch (RemoteException e) { - Log.w(TAG, "Error getting remote stream max volume", e); - return 0; - } + // TODO STOPSHIP switch callers to use media sessions instead + Log.e(TAG, "Need to implement new Remote Volume!"); + return 0; } /** @@ -3003,7 +2871,8 @@ public class AudioManager { /** @hide */ public static final int SUCCESS = AudioSystem.SUCCESS; - /** @hide + /** + * A default error code. */ public static final int ERROR = AudioSystem.ERROR; /** @hide @@ -3018,7 +2887,9 @@ public class AudioManager { /** @hide */ public static final int ERROR_NO_INIT = AudioSystem.NO_INIT; - /** @hide + /** + * An error code indicating that the object reporting it is no longer valid and needs to + * be recreated. */ public static final int ERROR_DEAD_OBJECT = AudioSystem.DEAD_OBJECT; diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index fd346d5..0c224a6 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -108,8 +108,7 @@ public class AudioService extends IAudioService.Stub { /** Debug volumes */ protected static final boolean DEBUG_VOL = false; - /** Reroute calls to media session apis */ - private static final boolean USE_SESSIONS = true; + /** debug calls to media session apis */ private static final boolean DEBUG_SESSIONS = true; /** Allow volume changes to set ringer mode to silent? */ @@ -177,7 +176,8 @@ public class AudioService extends IAudioService.Stub { // these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(), // and not with sendMsg(..., ..., SENDMSG_QUEUE, ...) private static final int MSG_SET_WIRED_DEVICE_CONNECTION_STATE = 100; - private static final int MSG_SET_A2DP_CONNECTION_STATE = 101; + private static final int MSG_SET_A2DP_SRC_CONNECTION_STATE = 101; + private static final int MSG_SET_A2DP_SINK_CONNECTION_STATE = 102; // end of messages handled under wakelock private static final int BTA2DP_DOCK_TIMEOUT_MILLIS = 8000; @@ -830,24 +830,6 @@ public class AudioService extends IAudioService.Stub { } /** @see AudioManager#adjustVolume(int, int) */ - public void adjustVolume(int direction, int flags, String callingPackage) { - adjustSuggestedStreamVolume(direction, AudioManager.USE_DEFAULT_STREAM_TYPE, flags, - callingPackage); - } - - /** @see AudioManager#adjustLocalOrRemoteStreamVolume(int, int) with current assumption - * on streamType: fixed to STREAM_MUSIC */ - public void adjustLocalOrRemoteStreamVolume(int streamType, int direction, - String callingPackage) { - if (DEBUG_VOL) Log.d(TAG, "adjustLocalOrRemoteStreamVolume(dir="+direction+")"); - if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) { - adjustStreamVolume(AudioSystem.STREAM_MUSIC, direction, 0, callingPackage); - } else if (mMediaFocusControl.checkUpdateRemoteStateIfActive(AudioSystem.STREAM_MUSIC)) { - mMediaFocusControl.adjustRemoteVolume(AudioSystem.STREAM_MUSIC, direction, 0); - } - } - - /** @see AudioManager#adjustVolume(int, int) */ public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags, String callingPackage) { if (DEBUG_VOL) Log.d(TAG, "adjustSuggestedStreamVolume() stream="+suggestedStreamType); @@ -1320,11 +1302,16 @@ public class AudioService extends IAudioService.Stub { } /** @see AudioManager#setMasterMute(boolean, int) */ - public void setMasterMute(boolean state, int flags, IBinder cb) { + public void setMasterMute(boolean state, int flags, String callingPackage, IBinder cb) { if (mUseFixedVolume) { return; } + if (mAppOps.noteOp(AppOpsManager.OP_AUDIO_MASTER_VOLUME, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return; + } + if (state != AudioSystem.getMasterMute()) { AudioSystem.setMasterMute(state); // Post a persist master volume msg @@ -1431,6 +1418,16 @@ public class AudioService extends IAudioService.Stub { } } + /** @see AudioManager#setMicrophoneMute(boolean) */ + public void setMicrophoneMute(boolean on, String callingPackage) { + if (mAppOps.noteOp(AppOpsManager.OP_MUTE_MICROPHONE, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return; + } + + AudioSystem.muteMicrophone(on); + } + /** @see AudioManager#getRingerMode() */ public int getRingerMode() { synchronized(mSettingsLock) { @@ -2059,7 +2056,19 @@ public class AudioService extends IAudioService.Stub { } /** @see AudioManager#startBluetoothSco() */ - public void startBluetoothSco(IBinder cb, int targetSdkVersion){ + public void startBluetoothSco(IBinder cb, int targetSdkVersion) { + int scoAudioMode = + (targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2) ? + SCO_MODE_VIRTUAL_CALL : SCO_MODE_RAW; + startBluetoothScoInt(cb, scoAudioMode); + } + + /** @see AudioManager#startBluetoothScoVirtualCall() */ + public void startBluetoothScoVirtualCall(IBinder cb) { + startBluetoothScoInt(cb, SCO_MODE_VIRTUAL_CALL); + } + + void startBluetoothScoInt(IBinder cb, int scoAudioMode){ if (!checkAudioSettingsPermission("startBluetoothSco()") || !mSystemReady) { return; @@ -2071,7 +2080,7 @@ public class AudioService extends IAudioService.Stub { // The caller identity must be cleared after getScoClient() because it is needed if a new // client is created. final long ident = Binder.clearCallingIdentity(); - client.incCount(targetSdkVersion); + client.incCount(scoAudioMode); Binder.restoreCallingIdentity(ident); } @@ -2117,9 +2126,9 @@ public class AudioService extends IAudioService.Stub { } } - public void incCount(int targetSdkVersion) { + public void incCount(int scoAudioMode) { synchronized(mScoClients) { - requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, targetSdkVersion); + requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode); if (mStartcount == 0) { try { mCb.linkToDeath(this, 0); @@ -2189,7 +2198,7 @@ public class AudioService extends IAudioService.Stub { } } - private void requestScoState(int state, int targetSdkVersion) { + private void requestScoState(int state, int scoAudioMode) { checkScoAudioState(); if (totalCount() == 0) { if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { @@ -2204,9 +2213,7 @@ public class AudioService extends IAudioService.Stub { (mScoAudioState == SCO_STATE_INACTIVE || mScoAudioState == SCO_STATE_DEACTIVATE_REQ)) { if (mScoAudioState == SCO_STATE_INACTIVE) { - mScoAudioMode = - (targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2) ? - SCO_MODE_VIRTUAL_CALL : SCO_MODE_RAW; + mScoAudioMode = scoAudioMode; if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null) { boolean status; if (mScoAudioMode == SCO_MODE_RAW) { @@ -2384,10 +2391,10 @@ public class AudioService extends IAudioService.Stub { synchronized (mConnectedDevices) { int state = mA2dp.getConnectionState(btDevice); int delay = checkSendBecomingNoisyIntent( - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, - (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0); + AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, + (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0); queueMsgUnderWakeLock(mAudioHandler, - MSG_SET_A2DP_CONNECTION_STATE, + MSG_SET_A2DP_SINK_CONNECTION_STATE, state, 0, btDevice, @@ -2397,6 +2404,22 @@ public class AudioService extends IAudioService.Stub { } break; + case BluetoothProfile.A2DP_SINK: + deviceList = proxy.getConnectedDevices(); + if (deviceList.size() > 0) { + btDevice = deviceList.get(0); + synchronized (mConnectedDevices) { + int state = proxy.getConnectionState(btDevice); + queueMsgUnderWakeLock(mAudioHandler, + MSG_SET_A2DP_SRC_CONNECTION_STATE, + state, + 0, + btDevice, + 0 /* delay */); + } + } + break; + case BluetoothProfile.HEADSET: synchronized (mScoClients) { // Discard timeout message @@ -2465,6 +2488,15 @@ public class AudioService extends IAudioService.Stub { } break; + case BluetoothProfile.A2DP_SINK: + synchronized (mConnectedDevices) { + if (mConnectedDevices.containsKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP)) { + makeA2dpSrcUnavailable( + mConnectedDevices.get(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP)); + } + } + break; + case BluetoothProfile.HEADSET: synchronized (mScoClients) { mBluetoothHeadset = null; @@ -2880,14 +2912,22 @@ public class AudioService extends IAudioService.Stub { } } - public int setBluetoothA2dpDeviceConnectionState(BluetoothDevice device, int state) + public int setBluetoothA2dpDeviceConnectionState(BluetoothDevice device, int state, int profile) { int delay; + if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) { + throw new IllegalArgumentException("invalid profile " + profile); + } synchronized (mConnectedDevices) { - delay = checkSendBecomingNoisyIntent(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, - (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0); + if (profile == BluetoothProfile.A2DP) { + delay = checkSendBecomingNoisyIntent(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, + (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0); + } else { + delay = 0; + } queueMsgUnderWakeLock(mAudioHandler, - MSG_SET_A2DP_CONNECTION_STATE, + (profile == BluetoothProfile.A2DP ? + MSG_SET_A2DP_SINK_CONNECTION_STATE : MSG_SET_A2DP_SRC_CONNECTION_STATE), state, 0, device, @@ -2933,54 +2973,56 @@ public class AudioService extends IAudioService.Stub { return name + "_" + suffix; } - public synchronized void readSettings() { - // force maximum volume on all streams if fixed volume property is set - if (mUseFixedVolume) { - mIndex.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax); - return; - } - // do not read system stream volume from settings: this stream is always aliased - // to another stream type and its volume is never persisted. Values in settings can - // only be stale values - if ((mStreamType == AudioSystem.STREAM_SYSTEM) || - (mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED)) { - int index = 10 * AudioManager.DEFAULT_STREAM_VOLUME[mStreamType]; - synchronized (mCameraSoundForced) { - if (mCameraSoundForced) { - index = mIndexMax; + public void readSettings() { + synchronized (VolumeStreamState.class) { + // force maximum volume on all streams if fixed volume property is set + if (mUseFixedVolume) { + mIndex.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax); + return; + } + // do not read system stream volume from settings: this stream is always aliased + // to another stream type and its volume is never persisted. Values in settings can + // only be stale values + if ((mStreamType == AudioSystem.STREAM_SYSTEM) || + (mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED)) { + int index = 10 * AudioManager.DEFAULT_STREAM_VOLUME[mStreamType]; + synchronized (mCameraSoundForced) { + if (mCameraSoundForced) { + index = mIndexMax; + } } + mIndex.put(AudioSystem.DEVICE_OUT_DEFAULT, index); + return; } - mIndex.put(AudioSystem.DEVICE_OUT_DEFAULT, index); - return; - } - int remainingDevices = AudioSystem.DEVICE_OUT_ALL; + int remainingDevices = AudioSystem.DEVICE_OUT_ALL; - for (int i = 0; remainingDevices != 0; i++) { - int device = (1 << i); - if ((device & remainingDevices) == 0) { - continue; - } - remainingDevices &= ~device; - - // retrieve current volume for device - String name = getSettingNameForDevice(device); - // if no volume stored for current stream and device, use default volume if default - // device, continue otherwise - int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT) ? - AudioManager.DEFAULT_STREAM_VOLUME[mStreamType] : -1; - int index = Settings.System.getIntForUser( - mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT); - if (index == -1) { - continue; - } + for (int i = 0; remainingDevices != 0; i++) { + int device = (1 << i); + if ((device & remainingDevices) == 0) { + continue; + } + remainingDevices &= ~device; + + // retrieve current volume for device + String name = getSettingNameForDevice(device); + // if no volume stored for current stream and device, use default volume if default + // device, continue otherwise + int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT) ? + AudioManager.DEFAULT_STREAM_VOLUME[mStreamType] : -1; + int index = Settings.System.getIntForUser( + mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT); + if (index == -1) { + continue; + } - // ignore settings for fixed volume devices: volume should always be at max or 0 - if ((mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_MUSIC) && - ((device & mFixedVolumeDevices) != 0)) { - mIndex.put(device, (index != 0) ? mIndexMax : 0); - } else { - mIndex.put(device, getValidIndex(10 * index)); + // ignore settings for fixed volume devices: volume should always be at max or 0 + if ((mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_MUSIC) && + ((device & mFixedVolumeDevices) != 0)) { + mIndex.put(device, (index != 0) ? mIndexMax : 0); + } else { + mIndex.put(device, getValidIndex(10 * index)); + } } } } @@ -2998,32 +3040,34 @@ public class AudioService extends IAudioService.Stub { AudioSystem.setStreamVolumeIndex(mStreamType, index, device); } - public synchronized void applyAllVolumes() { - // apply default volume first: by convention this will reset all - // devices volumes in audio policy manager to the supplied value - int index; - if (isMuted()) { - index = 0; - } else { - index = (getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5)/10; - } - AudioSystem.setStreamVolumeIndex(mStreamType, index, AudioSystem.DEVICE_OUT_DEFAULT); - // then apply device specific volumes - Set set = mIndex.entrySet(); - Iterator i = set.iterator(); - while (i.hasNext()) { - Map.Entry entry = (Map.Entry)i.next(); - int device = ((Integer)entry.getKey()).intValue(); - if (device != AudioSystem.DEVICE_OUT_DEFAULT) { - if (isMuted()) { - index = 0; - } else if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 && - mAvrcpAbsVolSupported) { - index = (mIndexMax + 5)/10; - } else { - index = ((Integer)entry.getValue() + 5)/10; + public void applyAllVolumes() { + synchronized (VolumeStreamState.class) { + // apply default volume first: by convention this will reset all + // devices volumes in audio policy manager to the supplied value + int index; + if (isMuted()) { + index = 0; + } else { + index = (getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5)/10; + } + AudioSystem.setStreamVolumeIndex(mStreamType, index, AudioSystem.DEVICE_OUT_DEFAULT); + // then apply device specific volumes + Set set = mIndex.entrySet(); + Iterator i = set.iterator(); + while (i.hasNext()) { + Map.Entry entry = (Map.Entry)i.next(); + int device = ((Integer)entry.getKey()).intValue(); + if (device != AudioSystem.DEVICE_OUT_DEFAULT) { + if (isMuted()) { + index = 0; + } else if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 && + mAvrcpAbsVolSupported) { + index = (mIndexMax + 5)/10; + } else { + index = ((Integer)entry.getValue() + 5)/10; + } + AudioSystem.setStreamVolumeIndex(mStreamType, index, device); } - AudioSystem.setStreamVolumeIndex(mStreamType, index, device); } } } @@ -3033,94 +3077,104 @@ public class AudioService extends IAudioService.Stub { device); } - public synchronized boolean setIndex(int index, int device) { - int oldIndex = getIndex(device); - index = getValidIndex(index); - synchronized (mCameraSoundForced) { - if ((mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) && mCameraSoundForced) { - index = mIndexMax; + public boolean setIndex(int index, int device) { + synchronized (VolumeStreamState.class) { + int oldIndex = getIndex(device); + index = getValidIndex(index); + synchronized (mCameraSoundForced) { + if ((mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) && mCameraSoundForced) { + index = mIndexMax; + } } - } - mIndex.put(device, index); - - if (oldIndex != index) { - // Apply change to all streams using this one as alias - // if changing volume of current device, also change volume of current - // device on aliased stream - boolean currentDevice = (device == getDeviceForStream(mStreamType)); - int numStreamTypes = AudioSystem.getNumStreamTypes(); - for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { - if (streamType != mStreamType && - mStreamVolumeAlias[streamType] == mStreamType) { - int scaledIndex = rescaleIndex(index, mStreamType, streamType); - mStreamStates[streamType].setIndex(scaledIndex, - device); - if (currentDevice) { + mIndex.put(device, index); + + if (oldIndex != index) { + // Apply change to all streams using this one as alias + // if changing volume of current device, also change volume of current + // device on aliased stream + boolean currentDevice = (device == getDeviceForStream(mStreamType)); + int numStreamTypes = AudioSystem.getNumStreamTypes(); + for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { + if (streamType != mStreamType && + mStreamVolumeAlias[streamType] == mStreamType) { + int scaledIndex = rescaleIndex(index, mStreamType, streamType); mStreamStates[streamType].setIndex(scaledIndex, - getDeviceForStream(streamType)); + device); + if (currentDevice) { + mStreamStates[streamType].setIndex(scaledIndex, + getDeviceForStream(streamType)); + } } } + return true; + } else { + return false; } - return true; - } else { - return false; } } - public synchronized int getIndex(int device) { - Integer index = mIndex.get(device); - if (index == null) { - // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT - index = mIndex.get(AudioSystem.DEVICE_OUT_DEFAULT); + public int getIndex(int device) { + synchronized (VolumeStreamState.class) { + Integer index = mIndex.get(device); + if (index == null) { + // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT + index = mIndex.get(AudioSystem.DEVICE_OUT_DEFAULT); + } + return index.intValue(); } - return index.intValue(); } public int getMaxIndex() { return mIndexMax; } - public synchronized void setAllIndexes(VolumeStreamState srcStream) { - int srcStreamType = srcStream.getStreamType(); - // apply default device volume from source stream to all devices first in case - // some devices are present in this stream state but not in source stream state - int index = srcStream.getIndex(AudioSystem.DEVICE_OUT_DEFAULT); - index = rescaleIndex(index, srcStreamType, mStreamType); - Set set = mIndex.entrySet(); - Iterator i = set.iterator(); - while (i.hasNext()) { - Map.Entry entry = (Map.Entry)i.next(); - entry.setValue(index); - } - // Now apply actual volume for devices in source stream state - set = srcStream.mIndex.entrySet(); - i = set.iterator(); - while (i.hasNext()) { - Map.Entry entry = (Map.Entry)i.next(); - int device = ((Integer)entry.getKey()).intValue(); - index = ((Integer)entry.getValue()).intValue(); + public void setAllIndexes(VolumeStreamState srcStream) { + synchronized (VolumeStreamState.class) { + int srcStreamType = srcStream.getStreamType(); + // apply default device volume from source stream to all devices first in case + // some devices are present in this stream state but not in source stream state + int index = srcStream.getIndex(AudioSystem.DEVICE_OUT_DEFAULT); index = rescaleIndex(index, srcStreamType, mStreamType); - - setIndex(index, device); + Set set = mIndex.entrySet(); + Iterator i = set.iterator(); + while (i.hasNext()) { + Map.Entry entry = (Map.Entry)i.next(); + entry.setValue(index); + } + // Now apply actual volume for devices in source stream state + set = srcStream.mIndex.entrySet(); + i = set.iterator(); + while (i.hasNext()) { + Map.Entry entry = (Map.Entry)i.next(); + int device = ((Integer)entry.getKey()).intValue(); + index = ((Integer)entry.getValue()).intValue(); + index = rescaleIndex(index, srcStreamType, mStreamType); + + setIndex(index, device); + } } } - public synchronized void setAllIndexesToMax() { - Set set = mIndex.entrySet(); - Iterator i = set.iterator(); - while (i.hasNext()) { - Map.Entry entry = (Map.Entry)i.next(); - entry.setValue(mIndexMax); + public void setAllIndexesToMax() { + synchronized (VolumeStreamState.class) { + Set set = mIndex.entrySet(); + Iterator i = set.iterator(); + while (i.hasNext()) { + Map.Entry entry = (Map.Entry)i.next(); + entry.setValue(mIndexMax); + } } } - public synchronized void mute(IBinder cb, boolean state) { - VolumeDeathHandler handler = getDeathHandler(cb, state); - if (handler == null) { - Log.e(TAG, "Could not get client death handler for stream: "+mStreamType); - return; + public void mute(IBinder cb, boolean state) { + synchronized (VolumeStreamState.class) { + VolumeDeathHandler handler = getDeathHandler(cb, state); + if (handler == null) { + Log.e(TAG, "Could not get client death handler for stream: "+mStreamType); + return; + } + handler.mute(state); } - handler.mute(state); } public int getStreamType() { @@ -3729,8 +3783,13 @@ public class AudioService extends IAudioService.Stub { mAudioEventWakeLock.release(); break; - case MSG_SET_A2DP_CONNECTION_STATE: - onSetA2dpConnectionState((BluetoothDevice)msg.obj, msg.arg1); + case MSG_SET_A2DP_SRC_CONNECTION_STATE: + onSetA2dpSourceConnectionState((BluetoothDevice)msg.obj, msg.arg1); + mAudioEventWakeLock.release(); + break; + + case MSG_SET_A2DP_SINK_CONNECTION_STATE: + onSetA2dpSinkConnectionState((BluetoothDevice)msg.obj, msg.arg1); mAudioEventWakeLock.release(); break; @@ -3857,6 +3916,23 @@ public class AudioService extends IAudioService.Stub { } // must be called synchronized on mConnectedDevices + private void makeA2dpSrcAvailable(String address) { + AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, + AudioSystem.DEVICE_STATE_AVAILABLE, + address); + mConnectedDevices.put( new Integer(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP), + address); + } + + // must be called synchronized on mConnectedDevices + private void makeA2dpSrcUnavailable(String address) { + AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, + AudioSystem.DEVICE_STATE_UNAVAILABLE, + address); + mConnectedDevices.remove(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP); + } + + // must be called synchronized on mConnectedDevices private void cancelA2dpDeviceTimeout() { mAudioHandler.removeMessages(MSG_BTA2DP_DOCK_TIMEOUT); } @@ -3866,9 +3942,11 @@ public class AudioService extends IAudioService.Stub { return mAudioHandler.hasMessages(MSG_BTA2DP_DOCK_TIMEOUT); } - private void onSetA2dpConnectionState(BluetoothDevice btDevice, int state) + private void onSetA2dpSinkConnectionState(BluetoothDevice btDevice, int state) { - if (DEBUG_VOL) Log.d(TAG, "onSetA2dpConnectionState btDevice="+btDevice+" state="+state); + if (DEBUG_VOL) { + Log.d(TAG, "onSetA2dpSinkConnectionState btDevice="+btDevice+"state="+state); + } if (btDevice == null) { return; } @@ -3927,6 +4005,32 @@ public class AudioService extends IAudioService.Stub { } } + private void onSetA2dpSourceConnectionState(BluetoothDevice btDevice, int state) + { + if (DEBUG_VOL) { + Log.d(TAG, "onSetA2dpSourceConnectionState btDevice="+btDevice+" state="+state); + } + if (btDevice == null) { + return; + } + String address = btDevice.getAddress(); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + address = ""; + } + + synchronized (mConnectedDevices) { + boolean isConnected = + (mConnectedDevices.containsKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP) && + mConnectedDevices.get(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP).equals(address)); + + if (isConnected && state != BluetoothProfile.STATE_CONNECTED) { + makeA2dpSrcUnavailable(address); + } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) { + makeA2dpSrcAvailable(address); + } + } + } + public void avrcpSupportsAbsoluteVolume(String address, boolean support) { // address is not used for now, but may be used when multiple a2dp devices are supported synchronized (mA2dpAvrcpLock) { @@ -3992,7 +4096,8 @@ public class AudioService extends IAudioService.Stub { } } - if (mAudioHandler.hasMessages(MSG_SET_A2DP_CONNECTION_STATE) || + if (mAudioHandler.hasMessages(MSG_SET_A2DP_SRC_CONNECTION_STATE) || + mAudioHandler.hasMessages(MSG_SET_A2DP_SINK_CONNECTION_STATE) || mAudioHandler.hasMessages(MSG_SET_WIRED_DEVICE_CONNECTION_STATE)) { delay = 1000; } @@ -4059,8 +4164,9 @@ public class AudioService extends IAudioService.Stub { (device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))) { setBluetoothA2dpOnInt(true); } - boolean isUsb = ((device & AudioSystem.DEVICE_OUT_ALL_USB) != 0) || - ((device & AudioSystem.DEVICE_IN_ALL_USB) != 0); + boolean isUsb = ((device & ~AudioSystem.DEVICE_OUT_ALL_USB) == 0) || + (((device & AudioSystem.DEVICE_BIT_IN) != 0) && + ((device & ~AudioSystem.DEVICE_IN_ALL_USB) == 0)); handleDeviceConnection((state == 1), device, (isUsb ? name : "")); if (state != 0) { if ((device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) || @@ -4077,7 +4183,7 @@ public class AudioService extends IAudioService.Stub { MUSIC_ACTIVE_POLL_PERIOD_MS); } } - if (!isUsb) { + if (!isUsb && (device != AudioSystem.DEVICE_IN_WIRED_HEADSET)) { sendDeviceConnectionIntent(device, state, name); } } @@ -4093,7 +4199,8 @@ public class AudioService extends IAudioService.Stub { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - int device; + int outDevice; + int inDevice; int state; if (action.equals(Intent.ACTION_DOCK_EVENT)) { @@ -4128,7 +4235,8 @@ public class AudioService extends IAudioService.Stub { } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED); - device = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO; + outDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO; + inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET; String address = null; BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); @@ -4142,10 +4250,10 @@ public class AudioService extends IAudioService.Stub { switch (btClass.getDeviceClass()) { case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: - device = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET; + outDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET; break; case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: - device = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT; + outDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT; break; } } @@ -4155,7 +4263,9 @@ public class AudioService extends IAudioService.Stub { } boolean connected = (state == BluetoothProfile.STATE_CONNECTED); - if (handleDeviceConnection(connected, device, address)) { + boolean success = handleDeviceConnection(connected, outDevice, address) && + handleDeviceConnection(connected, inDevice, address); + if (success) { synchronized (mScoClients) { if (connected) { mBluetoothHeadsetDevice = btDevice; @@ -4175,8 +4285,8 @@ public class AudioService extends IAudioService.Stub { : "card=" + alsaCard + ";device=" + alsaDevice); // Playback Device - device = AudioSystem.DEVICE_OUT_USB_ACCESSORY; - setWiredDeviceConnectionState(device, state, params); + outDevice = AudioSystem.DEVICE_OUT_USB_ACCESSORY; + setWiredDeviceConnectionState(outDevice, state, params); } else if (action.equals(Intent.ACTION_USB_AUDIO_DEVICE_PLUG)) { state = intent.getIntExtra("state", 0); @@ -4191,14 +4301,14 @@ public class AudioService extends IAudioService.Stub { // Playback Device if (hasPlayback) { - device = AudioSystem.DEVICE_OUT_USB_DEVICE; - setWiredDeviceConnectionState(device, state, params); + outDevice = AudioSystem.DEVICE_OUT_USB_DEVICE; + setWiredDeviceConnectionState(outDevice, state, params); } // Capture Device if (hasCapture) { - device = AudioSystem.DEVICE_IN_USB_DEVICE; - setWiredDeviceConnectionState(device, state, params); + inDevice = AudioSystem.DEVICE_IN_USB_DEVICE; + setWiredDeviceConnectionState(inDevice, state, params); } } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { boolean broadcast = false; @@ -4302,92 +4412,12 @@ public class AudioService extends IAudioService.Stub { mMediaFocusControl.remoteControlDisplayWantsPlaybackPositionSync(rcd, wantsSync); } - public void registerMediaButtonEventReceiverForCalls(ComponentName c) { - mMediaFocusControl.registerMediaButtonEventReceiverForCalls(c); - } - - public void unregisterMediaButtonEventReceiverForCalls() { - mMediaFocusControl.unregisterMediaButtonEventReceiverForCalls(); - } - - public void registerMediaButtonIntent(PendingIntent pi, ComponentName c, IBinder token) { - mMediaFocusControl.registerMediaButtonIntent(pi, c, token); - } - - public void unregisterMediaButtonIntent(PendingIntent pi) { - mMediaFocusControl.unregisterMediaButtonIntent(pi); - } - - public int registerRemoteControlClient(PendingIntent mediaIntent, - IRemoteControlClient rcClient, String callingPckg) { - return mMediaFocusControl.registerRemoteControlClient(mediaIntent, rcClient, callingPckg); - } - - public void unregisterRemoteControlClient(PendingIntent mediaIntent, - IRemoteControlClient rcClient) { - mMediaFocusControl.unregisterRemoteControlClient(mediaIntent, rcClient); - } - - public void setRemoteControlClientPlaybackPosition(int generationId, long timeMs) { - mMediaFocusControl.setRemoteControlClientPlaybackPosition(generationId, timeMs); - } - - public void updateRemoteControlClientMetadata(int generationId, int key, Rating value) { - mMediaFocusControl.updateRemoteControlClientMetadata(generationId, key, value); - } - - public void registerRemoteVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) { - mMediaFocusControl.registerRemoteVolumeObserverForRcc(rccId, rvo); - } - - @Override - public int getRemoteStreamVolume() { - return mMediaFocusControl.getRemoteStreamVolume(); - } - - @Override - public int getRemoteStreamMaxVolume() { - return mMediaFocusControl.getRemoteStreamMaxVolume(); - } - @Override public void setRemoteStreamVolume(int index) { enforceSelfOrSystemUI("set the remote stream volume"); mMediaFocusControl.setRemoteStreamVolume(index); } - public void setPlaybackStateForRcc(int rccId, int state, long timeMs, float speed) { - mMediaFocusControl.setPlaybackStateForRcc(rccId, state, timeMs, speed); - } - - public void setPlaybackInfoForRcc(int rccId, int what, int value) { - mMediaFocusControl.setPlaybackInfoForRcc(rccId, what, value); - } - - public void dispatchMediaKeyEvent(KeyEvent keyEvent) { - if (USE_SESSIONS) { - if (DEBUG_SESSIONS) { - int pid = getCallingPid(); - Log.w(TAG, "Call to dispatchMediaKeyEvent from " + pid); - } - MediaSessionLegacyHelper.getHelper(mContext).sendMediaButtonEvent(keyEvent, false); - } else { - mMediaFocusControl.dispatchMediaKeyEvent(keyEvent); - } - } - - public void dispatchMediaKeyEventUnderWakelock(KeyEvent keyEvent) { - if (USE_SESSIONS) { - if (DEBUG_SESSIONS) { - int pid = getCallingPid(); - Log.w(TAG, "Call to dispatchMediaKeyEventUnderWakelock from " + pid); - } - MediaSessionLegacyHelper.getHelper(mContext).sendMediaButtonEvent(keyEvent, true); - } else { - mMediaFocusControl.dispatchMediaKeyEventUnderWakelock(keyEvent); - } - } - //========================================================================================== // Audio Focus //========================================================================================== diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index af7a3e1..63ed10c 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -130,6 +130,11 @@ public class AudioSystem public static native boolean isSourceActive(int source); /* + * Returns a new unused audio session ID + */ + public static native int newAudioSessionId(); + + /* * Sets a group generic audio configuration parameters. The use of these parameters * are platform dependent, see libaudio * @@ -220,6 +225,7 @@ public class AudioSystem // audio device definitions: must be kept in sync with values in system/core/audio.h // + public static final int DEVICE_NONE = 0x0; // reserved bits public static final int DEVICE_BIT_IN = 0x80000000; public static final int DEVICE_BIT_DEFAULT = 0x40000000; @@ -300,7 +306,7 @@ public class AudioSystem public static final int DEVICE_IN_TV_TUNER = DEVICE_BIT_IN | 0x4000; public static final int DEVICE_IN_LINE = DEVICE_BIT_IN | 0x8000; public static final int DEVICE_IN_SPDIF = DEVICE_BIT_IN | 0x10000; - + public static final int DEVICE_IN_BLUETOOTH_A2DP = DEVICE_BIT_IN | 0x20000; public static final int DEVICE_IN_DEFAULT = DEVICE_BIT_IN | DEVICE_BIT_DEFAULT; public static final int DEVICE_IN_ALL = (DEVICE_IN_COMMUNICATION | @@ -320,6 +326,7 @@ public class AudioSystem DEVICE_IN_TV_TUNER | DEVICE_IN_LINE | DEVICE_IN_SPDIF | + DEVICE_IN_BLUETOOTH_A2DP | DEVICE_IN_DEFAULT); public static final int DEVICE_IN_ALL_SCO = DEVICE_IN_BLUETOOTH_SCO_HEADSET; public static final int DEVICE_IN_ALL_USB = (DEVICE_IN_USB_ACCESSORY | diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 8eb83e4..3a72833 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -457,25 +457,19 @@ public class AudioTrack //-------------- // audio format - switch (audioFormat) { - case AudioFormat.ENCODING_DEFAULT: - mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; - break; - case AudioFormat.ENCODING_PCM_16BIT: - case AudioFormat.ENCODING_PCM_8BIT: - case AudioFormat.ENCODING_PCM_FLOAT: - mAudioFormat = audioFormat; - break; - default: - throw new IllegalArgumentException("Unsupported sample encoding." - + " Should be ENCODING_PCM_8BIT or ENCODING_PCM_16BIT" - + " or ENCODING_PCM_FLOAT" - + "."); + if (audioFormat == AudioFormat.ENCODING_DEFAULT) { + audioFormat = AudioFormat.ENCODING_PCM_16BIT; + } + + if (!AudioFormat.isValidEncoding(audioFormat)) { + throw new IllegalArgumentException("Unsupported audio encoding."); } + mAudioFormat = audioFormat; //-------------- // audio load mode - if ( (mode != MODE_STREAM) && (mode != MODE_STATIC) ) { + if (((mode != MODE_STREAM) && (mode != MODE_STATIC)) || + ((mode != MODE_STREAM) && !AudioFormat.isEncodingLinearPcm(mAudioFormat))) { throw new IllegalArgumentException("Invalid mode."); } mDataLoadMode = mode; @@ -522,8 +516,13 @@ public class AudioTrack private void audioBuffSizeCheck(int audioBufferSize) { // NB: this section is only valid with PCM data. // To update when supporting compressed formats - int frameSizeInBytes = mChannelCount - * (AudioFormat.getBytesPerSample(mAudioFormat)); + int frameSizeInBytes; + if (AudioFormat.isEncodingLinearPcm(mAudioFormat)) { + frameSizeInBytes = mChannelCount + * (AudioFormat.getBytesPerSample(mAudioFormat)); + } else { + frameSizeInBytes = 1; + } if ((audioBufferSize % frameSizeInBytes != 0) || (audioBufferSize < 1)) { throw new IllegalArgumentException("Invalid audio buffer size."); } @@ -757,9 +756,7 @@ public class AudioTrack } } - if ((audioFormat != AudioFormat.ENCODING_PCM_16BIT) - && (audioFormat != AudioFormat.ENCODING_PCM_8BIT) - && (audioFormat != AudioFormat.ENCODING_PCM_FLOAT)) { + if (!AudioFormat.isValidEncoding(audioFormat)) { loge("getMinBufferSize(): Invalid audio format."); return ERROR_BAD_VALUE; } @@ -816,6 +813,8 @@ public class AudioTrack * with the estimated time when that frame was presented or is committed to * be presented. * In the case that no timestamp is available, any supplied instance is left unaltered. + * A timestamp may be temporarily unavailable while the audio clock is stabilizing, + * or during and immediately after a route change. */ // Add this text when the "on new timestamp" API is added: // Use if you need to get the most recent timestamp outside of the event callback handler. @@ -1162,7 +1161,9 @@ public class AudioTrack * @param sizeInBytes the number of bytes to read in audioData after the offset. * @return the number of bytes that were written or {@link #ERROR_INVALID_OPERATION} * if the object wasn't properly initialized, or {@link #ERROR_BAD_VALUE} if - * the parameters don't resolve to valid data and indexes. + * the parameters don't resolve to valid data and indexes, or + * {@link AudioManager#ERROR_DEAD_OBJECT} if the AudioTrack is not valid anymore and + * needs to be recreated. */ public int write(byte[] audioData, int offsetInBytes, int sizeInBytes) { @@ -1211,7 +1212,7 @@ public class AudioTrack public int write(short[] audioData, int offsetInShorts, int sizeInShorts) { - if (mState == STATE_UNINITIALIZED || mAudioFormat == AudioFormat.ENCODING_PCM_FLOAT) { + if (mState == STATE_UNINITIALIZED || mAudioFormat != AudioFormat.ENCODING_PCM_16BIT) { return ERROR_INVALID_OPERATION; } @@ -1471,7 +1472,6 @@ public class AudioTrack void onPeriodicNotification(AudioTrack track); } - //--------------------------------------------------------- // Inner classes //-------------------- diff --git a/media/java/android/media/CamcorderProfile.java b/media/java/android/media/CamcorderProfile.java index 511111c..f9e49c1 100644 --- a/media/java/android/media/CamcorderProfile.java +++ b/media/java/android/media/CamcorderProfile.java @@ -90,9 +90,14 @@ public class CamcorderProfile */ public static final int QUALITY_QVGA = 7; + /** + * Quality level corresponding to the 2160p (3840x2160) resolution. + */ + public static final int QUALITY_2160P = 8; + // Start and end of quality list private static final int QUALITY_LIST_START = QUALITY_LOW; - private static final int QUALITY_LIST_END = QUALITY_QVGA; + private static final int QUALITY_LIST_END = QUALITY_2160P; /** * Time lapse quality level corresponding to the lowest available resolution. @@ -134,9 +139,14 @@ public class CamcorderProfile */ public static final int QUALITY_TIME_LAPSE_QVGA = 1007; + /** + * Time lapse quality level corresponding to the 2160p (3840 x 2160) resolution. + */ + public static final int QUALITY_TIME_LAPSE_2160P = 1008; + // Start and end of timelapse quality list private static final int QUALITY_TIME_LAPSE_LIST_START = QUALITY_TIME_LAPSE_LOW; - private static final int QUALITY_TIME_LAPSE_LIST_END = QUALITY_TIME_LAPSE_QVGA; + private static final int QUALITY_TIME_LAPSE_LIST_END = QUALITY_TIME_LAPSE_2160P; /** * Default recording duration in seconds before the session is terminated. diff --git a/media/java/android/media/ClosedCaptionRenderer.java b/media/java/android/media/ClosedCaptionRenderer.java new file mode 100644 index 0000000..ec33c5c --- /dev/null +++ b/media/java/android/media/ClosedCaptionRenderer.java @@ -0,0 +1,1464 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Parcel; +import android.text.ParcelableSpan; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.CharacterStyle; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.text.style.UpdateAppearance; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.CaptioningManager; +import android.view.accessibility.CaptioningManager.CaptionStyle; +import android.view.accessibility.CaptioningManager.CaptioningChangeListener; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Vector; + +/** @hide */ +public class ClosedCaptionRenderer extends SubtitleController.Renderer { + private final Context mContext; + private ClosedCaptionWidget mRenderingWidget; + + public ClosedCaptionRenderer(Context context) { + mContext = context; + } + + @Override + public boolean supports(MediaFormat format) { + if (format.containsKey(MediaFormat.KEY_MIME)) { + return format.getString(MediaFormat.KEY_MIME).equals( + MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608); + } + return false; + } + + @Override + public SubtitleTrack createTrack(MediaFormat format) { + if (mRenderingWidget == null) { + mRenderingWidget = new ClosedCaptionWidget(mContext); + } + return new ClosedCaptionTrack(mRenderingWidget, format); + } +} + +/** @hide */ +class ClosedCaptionTrack extends SubtitleTrack { + private final ClosedCaptionWidget mRenderingWidget; + private final CCParser mCCParser; + + ClosedCaptionTrack(ClosedCaptionWidget renderingWidget, MediaFormat format) { + super(format); + + mRenderingWidget = renderingWidget; + mCCParser = new CCParser(renderingWidget); + } + + @Override + public void onData(byte[] data, boolean eos, long runID) { + mCCParser.parse(data); + } + + @Override + public RenderingWidget getRenderingWidget() { + return mRenderingWidget; + } + + @Override + public void updateView(Vector<Cue> activeCues) { + // Overriding with NO-OP, CC rendering by-passes this + } +} + +/** + * @hide + * + * CCParser processes CEA-608 closed caption data. + * + * It calls back into OnDisplayChangedListener upon + * display change with styled text for rendering. + * + */ +class CCParser { + public static final int MAX_ROWS = 15; + public static final int MAX_COLS = 32; + + private static final String TAG = "CCParser"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final int INVALID = -1; + + // EIA-CEA-608: Table 70 - Control Codes + private static final int RCL = 0x20; + private static final int BS = 0x21; + private static final int AOF = 0x22; + private static final int AON = 0x23; + private static final int DER = 0x24; + private static final int RU2 = 0x25; + private static final int RU3 = 0x26; + private static final int RU4 = 0x27; + private static final int FON = 0x28; + private static final int RDC = 0x29; + private static final int TR = 0x2a; + private static final int RTD = 0x2b; + private static final int EDM = 0x2c; + private static final int CR = 0x2d; + private static final int ENM = 0x2e; + private static final int EOC = 0x2f; + + // Transparent Space + private static final char TS = '\u00A0'; + + // Captioning Modes + private static final int MODE_UNKNOWN = 0; + private static final int MODE_PAINT_ON = 1; + private static final int MODE_ROLL_UP = 2; + private static final int MODE_POP_ON = 3; + private static final int MODE_TEXT = 4; + + private final DisplayListener mListener; + + private int mMode = MODE_PAINT_ON; + private int mRollUpSize = 4; + + private CCMemory mDisplay = new CCMemory(); + private CCMemory mNonDisplay = new CCMemory(); + private CCMemory mTextMem = new CCMemory(); + + CCParser(DisplayListener listener) { + mListener = listener; + } + + void parse(byte[] data) { + CCData[] ccData = CCData.fromByteArray(data); + + for (int i = 0; i < ccData.length; i++) { + if (DEBUG) { + Log.d(TAG, ccData[i].toString()); + } + + if (handleCtrlCode(ccData[i]) + || handleTabOffsets(ccData[i]) + || handlePACCode(ccData[i]) + || handleMidRowCode(ccData[i])) { + continue; + } + + handleDisplayableChars(ccData[i]); + } + } + + interface DisplayListener { + public void onDisplayChanged(SpannableStringBuilder[] styledTexts); + public CaptionStyle getCaptionStyle(); + } + + private CCMemory getMemory() { + // get the CC memory to operate on for current mode + switch (mMode) { + case MODE_POP_ON: + return mNonDisplay; + case MODE_TEXT: + // TODO(chz): support only caption mode for now, + // in text mode, dump everything to text mem. + return mTextMem; + case MODE_PAINT_ON: + case MODE_ROLL_UP: + return mDisplay; + default: + Log.w(TAG, "unrecoginized mode: " + mMode); + } + return mDisplay; + } + + private boolean handleDisplayableChars(CCData ccData) { + if (!ccData.isDisplayableChar()) { + return false; + } + + // Extended char includes 1 automatic backspace + if (ccData.isExtendedChar()) { + getMemory().bs(); + } + + getMemory().writeText(ccData.getDisplayText()); + + if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) { + updateDisplay(); + } + + return true; + } + + private boolean handleMidRowCode(CCData ccData) { + StyleCode m = ccData.getMidRow(); + if (m != null) { + getMemory().writeMidRowCode(m); + return true; + } + return false; + } + + private boolean handlePACCode(CCData ccData) { + PAC pac = ccData.getPAC(); + + if (pac != null) { + if (mMode == MODE_ROLL_UP) { + getMemory().moveBaselineTo(pac.getRow(), mRollUpSize); + } + getMemory().writePAC(pac); + return true; + } + + return false; + } + + private boolean handleTabOffsets(CCData ccData) { + int tabs = ccData.getTabOffset(); + + if (tabs > 0) { + getMemory().tab(tabs); + return true; + } + + return false; + } + + private boolean handleCtrlCode(CCData ccData) { + int ctrlCode = ccData.getCtrlCode(); + switch(ctrlCode) { + case RCL: + // select pop-on style + mMode = MODE_POP_ON; + break; + case BS: + getMemory().bs(); + break; + case DER: + getMemory().der(); + break; + case RU2: + case RU3: + case RU4: + mRollUpSize = (ctrlCode - 0x23); + // erase memory if currently in other style + if (mMode != MODE_ROLL_UP) { + mDisplay.erase(); + mNonDisplay.erase(); + } + // select roll-up style + mMode = MODE_ROLL_UP; + break; + case FON: + Log.i(TAG, "Flash On"); + break; + case RDC: + // select paint-on style + mMode = MODE_PAINT_ON; + break; + case TR: + mMode = MODE_TEXT; + mTextMem.erase(); + break; + case RTD: + mMode = MODE_TEXT; + break; + case EDM: + // erase display memory + mDisplay.erase(); + updateDisplay(); + break; + case CR: + if (mMode == MODE_ROLL_UP) { + getMemory().rollUp(mRollUpSize); + } else { + getMemory().cr(); + } + if (mMode == MODE_ROLL_UP) { + updateDisplay(); + } + break; + case ENM: + // erase non-display memory + mNonDisplay.erase(); + break; + case EOC: + // swap display/non-display memory + swapMemory(); + // switch to pop-on style + mMode = MODE_POP_ON; + updateDisplay(); + break; + case INVALID: + default: + // not handled + return false; + } + + // handled + return true; + } + + private void updateDisplay() { + if (mListener != null) { + CaptionStyle captionStyle = mListener.getCaptionStyle(); + mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle)); + } + } + + private void swapMemory() { + CCMemory temp = mDisplay; + mDisplay = mNonDisplay; + mNonDisplay = temp; + } + + private static class StyleCode { + static final int COLOR_WHITE = 0; + static final int COLOR_GREEN = 1; + static final int COLOR_BLUE = 2; + static final int COLOR_CYAN = 3; + static final int COLOR_RED = 4; + static final int COLOR_YELLOW = 5; + static final int COLOR_MAGENTA = 6; + static final int COLOR_INVALID = 7; + + static final int STYLE_ITALICS = 0x00000001; + static final int STYLE_UNDERLINE = 0x00000002; + + static final String[] mColorMap = { + "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID" + }; + + final int mStyle; + final int mColor; + + static StyleCode fromByte(byte data2) { + int style = 0; + int color = (data2 >> 1) & 0x7; + + if ((data2 & 0x1) != 0) { + style |= STYLE_UNDERLINE; + } + + if (color == COLOR_INVALID) { + // WHITE ITALICS + color = COLOR_WHITE; + style |= STYLE_ITALICS; + } + + return new StyleCode(style, color); + } + + StyleCode(int style, int color) { + mStyle = style; + mColor = color; + } + + boolean isItalics() { + return (mStyle & STYLE_ITALICS) != 0; + } + + boolean isUnderline() { + return (mStyle & STYLE_UNDERLINE) != 0; + } + + int getColor() { + return mColor; + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + str.append("{"); + str.append(mColorMap[mColor]); + if ((mStyle & STYLE_ITALICS) != 0) { + str.append(", ITALICS"); + } + if ((mStyle & STYLE_UNDERLINE) != 0) { + str.append(", UNDERLINE"); + } + str.append("}"); + + return str.toString(); + } + } + + private static class PAC extends StyleCode { + final int mRow; + final int mCol; + + static PAC fromBytes(byte data1, byte data2) { + int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9}; + int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5); + int style = 0; + if ((data2 & 1) != 0) { + style |= STYLE_UNDERLINE; + } + if ((data2 & 0x10) != 0) { + // indent code + int indent = (data2 >> 1) & 0x7; + return new PAC(row, indent * 4, style, COLOR_WHITE); + } else { + // style code + int color = (data2 >> 1) & 0x7; + + if (color == COLOR_INVALID) { + // WHITE ITALICS + color = COLOR_WHITE; + style |= STYLE_ITALICS; + } + return new PAC(row, -1, style, color); + } + } + + PAC(int row, int col, int style, int color) { + super(style, color); + mRow = row; + mCol = col; + } + + boolean isIndentPAC() { + return (mCol >= 0); + } + + int getRow() { + return mRow; + } + + int getCol() { + return mCol; + } + + @Override + public String toString() { + return String.format("{%d, %d}, %s", + mRow, mCol, super.toString()); + } + } + + /* CCLineBuilder keeps track of displayable chars, as well as + * MidRow styles and PACs, for a single line of CC memory. + * + * It generates styled text via getStyledText() method. + */ + private static class CCLineBuilder { + private final StringBuilder mDisplayChars; + private final StyleCode[] mMidRowStyles; + private final StyleCode[] mPACStyles; + + CCLineBuilder(String str) { + mDisplayChars = new StringBuilder(str); + mMidRowStyles = new StyleCode[mDisplayChars.length()]; + mPACStyles = new StyleCode[mDisplayChars.length()]; + } + + void setCharAt(int index, char ch) { + mDisplayChars.setCharAt(index, ch); + mMidRowStyles[index] = null; + } + + void setMidRowAt(int index, StyleCode m) { + mDisplayChars.setCharAt(index, ' '); + mMidRowStyles[index] = m; + } + + void setPACAt(int index, PAC pac) { + mPACStyles[index] = pac; + } + + char charAt(int index) { + return mDisplayChars.charAt(index); + } + + int length() { + return mDisplayChars.length(); + } + + void applyStyleSpan( + SpannableStringBuilder styledText, + StyleCode s, int start, int end) { + if (s.isItalics()) { + styledText.setSpan( + new StyleSpan(android.graphics.Typeface.ITALIC), + start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (s.isUnderline()) { + styledText.setSpan( + new UnderlineSpan(), + start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + SpannableStringBuilder getStyledText(CaptionStyle captionStyle) { + SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars); + int start = -1, next = 0; + int styleStart = -1; + StyleCode curStyle = null; + while (next < mDisplayChars.length()) { + StyleCode newStyle = null; + if (mMidRowStyles[next] != null) { + // apply mid-row style change + newStyle = mMidRowStyles[next]; + } else if (mPACStyles[next] != null + && (styleStart < 0 || start < 0)) { + // apply PAC style change, only if: + // 1. no style set, or + // 2. style set, but prev char is none-displayable + newStyle = mPACStyles[next]; + } + if (newStyle != null) { + curStyle = newStyle; + if (styleStart >= 0 && start >= 0) { + applyStyleSpan(styledText, newStyle, styleStart, next); + } + styleStart = next; + } + + if (mDisplayChars.charAt(next) != TS) { + if (start < 0) { + start = next; + } + } else if (start >= 0) { + int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1; + int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1; + styledText.setSpan( + new MutableBackgroundColorSpan(captionStyle.backgroundColor), + expandedStart, expandedEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (styleStart >= 0) { + applyStyleSpan(styledText, curStyle, styleStart, expandedEnd); + } + start = -1; + } + next++; + } + + return styledText; + } + } + + /* + * CCMemory models a console-style display. + */ + private static class CCMemory { + private final String mBlankLine; + private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2]; + private int mRow; + private int mCol; + + CCMemory() { + char[] blank = new char[MAX_COLS + 2]; + Arrays.fill(blank, TS); + mBlankLine = new String(blank); + } + + void erase() { + // erase all lines + for (int i = 0; i < mLines.length; i++) { + mLines[i] = null; + } + mRow = MAX_ROWS; + mCol = 1; + } + + void der() { + if (mLines[mRow] != null) { + for (int i = 0; i < mCol; i++) { + if (mLines[mRow].charAt(i) != TS) { + for (int j = mCol; j < mLines[mRow].length(); j++) { + mLines[j].setCharAt(j, TS); + } + return; + } + } + mLines[mRow] = null; + } + } + + void tab(int tabs) { + moveCursorByCol(tabs); + } + + void bs() { + moveCursorByCol(-1); + if (mLines[mRow] != null) { + mLines[mRow].setCharAt(mCol, TS); + if (mCol == MAX_COLS - 1) { + // Spec recommendation: + // if cursor was at col 32, move cursor + // back to col 31 and erase both col 31&32 + mLines[mRow].setCharAt(MAX_COLS, TS); + } + } + } + + void cr() { + moveCursorTo(mRow + 1, 1); + } + + void rollUp(int windowSize) { + int i; + for (i = 0; i <= mRow - windowSize; i++) { + mLines[i] = null; + } + int startRow = mRow - windowSize + 1; + if (startRow < 1) { + startRow = 1; + } + for (i = startRow; i < mRow; i++) { + mLines[i] = mLines[i + 1]; + } + for (i = mRow; i < mLines.length; i++) { + // clear base row + mLines[i] = null; + } + // default to col 1, in case PAC is not sent + mCol = 1; + } + + void writeText(String text) { + for (int i = 0; i < text.length(); i++) { + getLineBuffer(mRow).setCharAt(mCol, text.charAt(i)); + moveCursorByCol(1); + } + } + + void writeMidRowCode(StyleCode m) { + getLineBuffer(mRow).setMidRowAt(mCol, m); + moveCursorByCol(1); + } + + void writePAC(PAC pac) { + if (pac.isIndentPAC()) { + moveCursorTo(pac.getRow(), pac.getCol()); + } else { + moveCursorToRow(pac.getRow()); + } + getLineBuffer(mRow).setPACAt(mCol, pac); + } + + SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) { + ArrayList<SpannableStringBuilder> rows = + new ArrayList<SpannableStringBuilder>(MAX_ROWS); + for (int i = 1; i <= MAX_ROWS; i++) { + rows.add(mLines[i] != null ? + mLines[i].getStyledText(captionStyle) : null); + } + return rows.toArray(new SpannableStringBuilder[MAX_ROWS]); + } + + private static int clamp(int x, int min, int max) { + return x < min ? min : (x > max ? max : x); + } + + private void moveCursorTo(int row, int col) { + mRow = clamp(row, 1, MAX_ROWS); + mCol = clamp(col, 1, MAX_COLS); + } + + private void moveCursorToRow(int row) { + mRow = clamp(row, 1, MAX_ROWS); + } + + private void moveCursorByCol(int col) { + mCol = clamp(mCol + col, 1, MAX_COLS); + } + + private void moveBaselineTo(int baseRow, int windowSize) { + if (mRow == baseRow) { + return; + } + int actualWindowSize = windowSize; + if (baseRow < actualWindowSize) { + actualWindowSize = baseRow; + } + if (mRow < actualWindowSize) { + actualWindowSize = mRow; + } + + int i; + if (baseRow < mRow) { + // copy from bottom to top row + for (i = actualWindowSize - 1; i >= 0; i--) { + mLines[baseRow - i] = mLines[mRow - i]; + } + } else { + // copy from top to bottom row + for (i = 0; i < actualWindowSize; i++) { + mLines[baseRow - i] = mLines[mRow - i]; + } + } + // clear rest of the rows + for (i = 0; i <= baseRow - windowSize; i++) { + mLines[i] = null; + } + for (i = baseRow + 1; i < mLines.length; i++) { + mLines[i] = null; + } + } + + private CCLineBuilder getLineBuffer(int row) { + if (mLines[row] == null) { + mLines[row] = new CCLineBuilder(mBlankLine); + } + return mLines[row]; + } + } + + /* + * CCData parses the raw CC byte pair into displayable chars, + * misc control codes, Mid-Row or Preamble Address Codes. + */ + private static class CCData { + private final byte mType; + private final byte mData1; + private final byte mData2; + + private static final String[] mCtrlCodeMap = { + "RCL", "BS" , "AOF", "AON", + "DER", "RU2", "RU3", "RU4", + "FON", "RDC", "TR" , "RTD", + "EDM", "CR" , "ENM", "EOC", + }; + + private static final String[] mSpecialCharMap = { + "\u00AE", + "\u00B0", + "\u00BD", + "\u00BF", + "\u2122", + "\u00A2", + "\u00A3", + "\u266A", // Eighth note + "\u00E0", + "\u00A0", // Transparent space + "\u00E8", + "\u00E2", + "\u00EA", + "\u00EE", + "\u00F4", + "\u00FB", + }; + + private static final String[] mSpanishCharMap = { + // Spanish and misc chars + "\u00C1", // A + "\u00C9", // E + "\u00D3", // I + "\u00DA", // O + "\u00DC", // U + "\u00FC", // u + "\u2018", // opening single quote + "\u00A1", // inverted exclamation mark + "*", + "'", + "\u2014", // em dash + "\u00A9", // Copyright + "\u2120", // Servicemark + "\u2022", // round bullet + "\u201C", // opening double quote + "\u201D", // closing double quote + // French + "\u00C0", + "\u00C2", + "\u00C7", + "\u00C8", + "\u00CA", + "\u00CB", + "\u00EB", + "\u00CE", + "\u00CF", + "\u00EF", + "\u00D4", + "\u00D9", + "\u00F9", + "\u00DB", + "\u00AB", + "\u00BB" + }; + + private static final String[] mProtugueseCharMap = { + // Portuguese + "\u00C3", + "\u00E3", + "\u00CD", + "\u00CC", + "\u00EC", + "\u00D2", + "\u00F2", + "\u00D5", + "\u00F5", + "{", + "}", + "\\", + "^", + "_", + "|", + "~", + // German and misc chars + "\u00C4", + "\u00E4", + "\u00D6", + "\u00F6", + "\u00DF", + "\u00A5", + "\u00A4", + "\u2502", // vertical bar + "\u00C5", + "\u00E5", + "\u00D8", + "\u00F8", + "\u250C", // top-left corner + "\u2510", // top-right corner + "\u2514", // lower-left corner + "\u2518", // lower-right corner + }; + + static CCData[] fromByteArray(byte[] data) { + CCData[] ccData = new CCData[data.length / 3]; + + for (int i = 0; i < ccData.length; i++) { + ccData[i] = new CCData( + data[i * 3], + data[i * 3 + 1], + data[i * 3 + 2]); + } + + return ccData; + } + + CCData(byte type, byte data1, byte data2) { + mType = type; + mData1 = data1; + mData2 = data2; + } + + int getCtrlCode() { + if ((mData1 == 0x14 || mData1 == 0x1c) + && mData2 >= 0x20 && mData2 <= 0x2f) { + return mData2; + } + return INVALID; + } + + StyleCode getMidRow() { + // only support standard Mid-row codes, ignore + // optional background/foreground mid-row codes + if ((mData1 == 0x11 || mData1 == 0x19) + && mData2 >= 0x20 && mData2 <= 0x2f) { + return StyleCode.fromByte(mData2); + } + return null; + } + + PAC getPAC() { + if ((mData1 & 0x70) == 0x10 + && (mData2 & 0x40) == 0x40 + && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) { + return PAC.fromBytes(mData1, mData2); + } + return null; + } + + int getTabOffset() { + if ((mData1 == 0x17 || mData1 == 0x1f) + && mData2 >= 0x21 && mData2 <= 0x23) { + return mData2 & 0x3; + } + return 0; + } + + boolean isDisplayableChar() { + return isBasicChar() || isSpecialChar() || isExtendedChar(); + } + + String getDisplayText() { + String str = getBasicChars(); + + if (str == null) { + str = getSpecialChar(); + + if (str == null) { + str = getExtendedChar(); + } + } + + return str; + } + + private String ctrlCodeToString(int ctrlCode) { + return mCtrlCodeMap[ctrlCode - 0x20]; + } + + private boolean isBasicChar() { + return mData1 >= 0x20 && mData1 <= 0x7f; + } + + private boolean isSpecialChar() { + return ((mData1 == 0x11 || mData1 == 0x19) + && mData2 >= 0x30 && mData2 <= 0x3f); + } + + private boolean isExtendedChar() { + return ((mData1 == 0x12 || mData1 == 0x1A + || mData1 == 0x13 || mData1 == 0x1B) + && mData2 >= 0x20 && mData2 <= 0x3f); + } + + private char getBasicChar(byte data) { + char c; + // replace the non-ASCII ones + switch (data) { + case 0x2A: c = '\u00E1'; break; + case 0x5C: c = '\u00E9'; break; + case 0x5E: c = '\u00ED'; break; + case 0x5F: c = '\u00F3'; break; + case 0x60: c = '\u00FA'; break; + case 0x7B: c = '\u00E7'; break; + case 0x7C: c = '\u00F7'; break; + case 0x7D: c = '\u00D1'; break; + case 0x7E: c = '\u00F1'; break; + case 0x7F: c = '\u2588'; break; // Full block + default: c = (char) data; break; + } + return c; + } + + private String getBasicChars() { + if (mData1 >= 0x20 && mData1 <= 0x7f) { + StringBuilder builder = new StringBuilder(2); + builder.append(getBasicChar(mData1)); + if (mData2 >= 0x20 && mData2 <= 0x7f) { + builder.append(getBasicChar(mData2)); + } + return builder.toString(); + } + + return null; + } + + private String getSpecialChar() { + if ((mData1 == 0x11 || mData1 == 0x19) + && mData2 >= 0x30 && mData2 <= 0x3f) { + return mSpecialCharMap[mData2 - 0x30]; + } + + return null; + } + + private String getExtendedChar() { + if ((mData1 == 0x12 || mData1 == 0x1A) + && mData2 >= 0x20 && mData2 <= 0x3f){ + // 1 Spanish/French char + return mSpanishCharMap[mData2 - 0x20]; + } else if ((mData1 == 0x13 || mData1 == 0x1B) + && mData2 >= 0x20 && mData2 <= 0x3f){ + // 1 Portuguese/German/Danish char + return mProtugueseCharMap[mData2 - 0x20]; + } + + return null; + } + + @Override + public String toString() { + String str; + + if (mData1 < 0x10 && mData2 < 0x10) { + // Null Pad, ignore + return String.format("[%d]Null: %02x %02x", mType, mData1, mData2); + } + + int ctrlCode = getCtrlCode(); + if (ctrlCode != INVALID) { + return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode)); + } + + int tabOffset = getTabOffset(); + if (tabOffset > 0) { + return String.format("[%d]Tab%d", mType, tabOffset); + } + + PAC pac = getPAC(); + if (pac != null) { + return String.format("[%d]PAC: %s", mType, pac.toString()); + } + + StyleCode m = getMidRow(); + if (m != null) { + return String.format("[%d]Mid-row: %s", mType, m.toString()); + } + + if (isDisplayableChar()) { + return String.format("[%d]Displayable: %s (%02x %02x)", + mType, getDisplayText(), mData1, mData2); + } + + return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2); + } + } +} + +/** + * @hide + * + * MutableBackgroundColorSpan + * + * This is a mutable version of BackgroundSpan to facilitate text + * rendering with edge styles. + * + */ +class MutableBackgroundColorSpan extends CharacterStyle + implements UpdateAppearance, ParcelableSpan { + private int mColor; + + public MutableBackgroundColorSpan(int color) { + mColor = color; + } + public MutableBackgroundColorSpan(Parcel src) { + mColor = src.readInt(); + } + public void setBackgroundColor(int color) { + mColor = color; + } + public int getBackgroundColor() { + return mColor; + } + @Override + public int getSpanTypeId() { + return TextUtils.BACKGROUND_COLOR_SPAN; + } + @Override + public int describeContents() { + return 0; + } + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mColor); + } + @Override + public void updateDrawState(TextPaint ds) { + ds.bgColor = mColor; + } +} + +/** + * Widget capable of rendering CEA-608 closed captions. + * + * @hide + */ +class ClosedCaptionWidget extends ViewGroup implements + SubtitleTrack.RenderingWidget, + CCParser.DisplayListener { + private static final String TAG = "ClosedCaptionWidget"; + + private static final Rect mTextBounds = new Rect(); + private static final String mDummyText = "1234567890123456789012345678901234"; + private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; + + /** Captioning manager, used to obtain and track caption properties. */ + private final CaptioningManager mManager; + + /** Callback for rendering changes. */ + private OnChangedListener mListener; + + /** Current caption style. */ + private CaptionStyle mCaptionStyle; + + /* Closed caption layout. */ + private CCLayout mClosedCaptionLayout; + + /** Whether a caption style change listener is registered. */ + private boolean mHasChangeListener; + + public ClosedCaptionWidget(Context context) { + this(context, null); + } + + public ClosedCaptionWidget(Context context, AttributeSet attrs) { + this(context, null, 0); + } + + public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Cannot render text over video when layer type is hardware. + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); + mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle()); + + mClosedCaptionLayout = new CCLayout(context); + mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); + addView(mClosedCaptionLayout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + requestLayout(); + } + + @Override + public void setOnChangedListener(OnChangedListener listener) { + mListener = listener; + } + + @Override + public void setSize(int width, int height) { + final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + + measure(widthSpec, heightSpec); + layout(0, 0, width, height); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + setVisibility(View.VISIBLE); + } else { + setVisibility(View.GONE); + } + + manageChangeListener(); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + manageChangeListener(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + manageChangeListener(); + } + + @Override + public void onDisplayChanged(SpannableStringBuilder[] styledTexts) { + mClosedCaptionLayout.update(styledTexts); + + if (mListener != null) { + mListener.onChanged(this); + } + } + + @Override + public CaptionStyle getCaptionStyle() { + return mCaptionStyle; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + mClosedCaptionLayout.measure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mClosedCaptionLayout.layout(l, t, r, b); + } + + /** + * Manages whether this renderer is listening for caption style changes. + */ + private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() { + @Override + public void onUserStyleChanged(CaptionStyle userStyle) { + mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle); + mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); + } + }; + + private void manageChangeListener() { + final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE; + if (mHasChangeListener != needsListener) { + mHasChangeListener = needsListener; + + if (needsListener) { + mManager.addCaptioningChangeListener(mCaptioningListener); + } else { + mManager.removeCaptioningChangeListener(mCaptioningListener); + } + } + } + + private static class CCLineBox extends TextView { + private static final float FONT_PADDING_RATIO = 0.75f; + private static final float EDGE_OUTLINE_RATIO = 0.1f; + private static final float EDGE_SHADOW_RATIO = 0.05f; + private float mOutlineWidth; + private float mShadowRadius; + private float mShadowOffset; + + private int mTextColor = Color.WHITE; + private int mBgColor = Color.BLACK; + private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE; + private int mEdgeColor = Color.TRANSPARENT; + + CCLineBox(Context context) { + super(context); + setGravity(Gravity.CENTER); + setBackgroundColor(Color.TRANSPARENT); + setTextColor(Color.WHITE); + setTypeface(Typeface.MONOSPACE); + setVisibility(View.INVISIBLE); + + final Resources res = getContext().getResources(); + + // get the default (will be updated later during measure) + mOutlineWidth = res.getDimensionPixelSize( + com.android.internal.R.dimen.subtitle_outline_width); + mShadowRadius = res.getDimensionPixelSize( + com.android.internal.R.dimen.subtitle_shadow_radius); + mShadowOffset = res.getDimensionPixelSize( + com.android.internal.R.dimen.subtitle_shadow_offset); + } + + void setCaptionStyle(CaptionStyle captionStyle) { + mTextColor = captionStyle.foregroundColor; + mBgColor = captionStyle.backgroundColor; + mEdgeType = captionStyle.edgeType; + mEdgeColor = captionStyle.edgeColor; + + setTextColor(mTextColor); + if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { + setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor); + } else { + setShadowLayer(0, 0, 0, 0); + } + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + float fontSize = MeasureSpec.getSize(heightMeasureSpec) + * FONT_PADDING_RATIO; + setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize); + + mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f; + mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;; + mShadowOffset = mShadowRadius; + + // set font scale in the X direction to match the required width + setScaleX(1.0f); + getPaint().getTextBounds(mDummyText, 0, mDummyText.length(), mTextBounds); + float actualTextWidth = mTextBounds.width(); + float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec); + setScaleX(requiredTextWidth / actualTextWidth); + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onDraw(Canvas c) { + if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED + || mEdgeType == CaptionStyle.EDGE_TYPE_NONE + || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { + // these edge styles don't require a second pass + super.onDraw(c); + return; + } + + if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) { + drawEdgeOutline(c); + } else { + // Raised or depressed + drawEdgeRaisedOrDepressed(c); + } + } + + private void drawEdgeOutline(Canvas c) { + TextPaint textPaint = getPaint(); + + Paint.Style previousStyle = textPaint.getStyle(); + Paint.Join previousJoin = textPaint.getStrokeJoin(); + float previousWidth = textPaint.getStrokeWidth(); + + setTextColor(mEdgeColor); + textPaint.setStyle(Paint.Style.FILL_AND_STROKE); + textPaint.setStrokeJoin(Paint.Join.ROUND); + textPaint.setStrokeWidth(mOutlineWidth); + + // Draw outline and background only. + super.onDraw(c); + + // Restore original settings. + setTextColor(mTextColor); + textPaint.setStyle(previousStyle); + textPaint.setStrokeJoin(previousJoin); + textPaint.setStrokeWidth(previousWidth); + + // Remove the background. + setBackgroundSpans(Color.TRANSPARENT); + // Draw foreground only. + super.onDraw(c); + // Restore the background. + setBackgroundSpans(mBgColor); + } + + private void drawEdgeRaisedOrDepressed(Canvas c) { + TextPaint textPaint = getPaint(); + + Paint.Style previousStyle = textPaint.getStyle(); + textPaint.setStyle(Paint.Style.FILL); + + final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED; + final int colorUp = raised ? Color.WHITE : mEdgeColor; + final int colorDown = raised ? mEdgeColor : Color.WHITE; + final float offset = mShadowRadius / 2f; + + // Draw background and text with shadow up + setShadowLayer(mShadowRadius, -offset, -offset, colorUp); + super.onDraw(c); + + // Remove the background. + setBackgroundSpans(Color.TRANSPARENT); + + // Draw text with shadow down + setShadowLayer(mShadowRadius, +offset, +offset, colorDown); + super.onDraw(c); + + // Restore settings + textPaint.setStyle(previousStyle); + + // Restore the background. + setBackgroundSpans(mBgColor); + } + + private void setBackgroundSpans(int color) { + CharSequence text = getText(); + if (text instanceof Spannable) { + Spannable spannable = (Spannable) text; + MutableBackgroundColorSpan[] bgSpans = spannable.getSpans( + 0, spannable.length(), MutableBackgroundColorSpan.class); + for (int i = 0; i < bgSpans.length; i++) { + bgSpans[i].setBackgroundColor(color); + } + } + } + } + + private static class CCLayout extends LinearLayout { + private static final int MAX_ROWS = CCParser.MAX_ROWS; + private static final float SAFE_AREA_RATIO = 0.9f; + + private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS]; + + CCLayout(Context context) { + super(context); + setGravity(Gravity.START); + setOrientation(LinearLayout.VERTICAL); + for (int i = 0; i < MAX_ROWS; i++) { + mLineBoxes[i] = new CCLineBox(getContext()); + addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + } + + void setCaptionStyle(CaptionStyle captionStyle) { + for (int i = 0; i < MAX_ROWS; i++) { + mLineBoxes[i].setCaptionStyle(captionStyle); + } + } + + void update(SpannableStringBuilder[] textBuffer) { + for (int i = 0; i < MAX_ROWS; i++) { + if (textBuffer[i] != null) { + mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE); + mLineBoxes[i].setVisibility(View.VISIBLE); + } else { + mLineBoxes[i].setVisibility(View.INVISIBLE); + } + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int safeWidth = getMeasuredWidth(); + int safeHeight = getMeasuredHeight(); + + // CEA-608 assumes 4:3 video + if (safeWidth * 3 >= safeHeight * 4) { + safeWidth = safeHeight * 4 / 3; + } else { + safeHeight = safeWidth * 3 / 4; + } + safeWidth *= SAFE_AREA_RATIO; + safeHeight *= SAFE_AREA_RATIO; + + int lineHeight = safeHeight / MAX_ROWS; + int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lineHeight, MeasureSpec.EXACTLY); + int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + safeWidth, MeasureSpec.EXACTLY); + + for (int i = 0; i < MAX_ROWS; i++) { + mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // safe caption area + int viewPortWidth = r - l; + int viewPortHeight = b - t; + int safeWidth, safeHeight; + // CEA-608 assumes 4:3 video + if (viewPortWidth * 3 >= viewPortHeight * 4) { + safeWidth = viewPortHeight * 4 / 3; + safeHeight = viewPortHeight; + } else { + safeWidth = viewPortWidth; + safeHeight = viewPortWidth * 3 / 4; + } + safeWidth *= SAFE_AREA_RATIO; + safeHeight *= SAFE_AREA_RATIO; + int left = (viewPortWidth - safeWidth) / 2; + int top = (viewPortHeight - safeHeight) / 2; + + for (int i = 0; i < MAX_ROWS; i++) { + mLineBoxes[i].layout( + left, + top + safeHeight * i / MAX_ROWS, + left + safeWidth, + top + safeHeight * (i + 1) / MAX_ROWS); + } + } + } +}; diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 30de4f9..4dcdd19 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -36,13 +36,8 @@ import android.view.KeyEvent; */ interface IAudioService { - void adjustVolume(int direction, int flags, String callingPackage); - boolean isLocalOrRemoteMusicActive(); - oneway void adjustLocalOrRemoteStreamVolume(int streamType, int direction, - String callingPackage); - void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags, String callingPackage); @@ -62,7 +57,7 @@ interface IAudioService { boolean isStreamMute(int streamType); - void setMasterMute(boolean state, int flags, IBinder cb); + void setMasterMute(boolean state, int flags, String callingPackage, IBinder cb); boolean isMasterMute(); @@ -78,6 +73,8 @@ interface IAudioService { int getLastAudibleMasterVolume(); + void setMicrophoneMute(boolean on, String callingPackage); + void setRingerMode(int ringerMode); int getRingerMode(); @@ -125,15 +122,6 @@ interface IAudioService { int getCurrentAudioFocus(); - oneway void dispatchMediaKeyEvent(in KeyEvent keyEvent); - void dispatchMediaKeyEventUnderWakelock(in KeyEvent keyEvent); - - void registerMediaButtonIntent(in PendingIntent pi, in ComponentName c, IBinder token); - oneway void unregisterMediaButtonIntent(in PendingIntent pi); - - oneway void registerMediaButtonEventReceiverForCalls(in ComponentName c); - oneway void unregisterMediaButtonEventReceiverForCalls(); - /** * Register an IRemoteControlDisplay. * Success of registration is subject to a check on @@ -186,43 +174,9 @@ interface IAudioService { */ oneway void remoteControlDisplayWantsPlaybackPositionSync(in IRemoteControlDisplay rcd, boolean wantsSync); - /** - * Request the user of a RemoteControlClient to seek to the given playback position. - * @param generationId the RemoteControlClient generation counter for which this request is - * issued. Requests for an older generation than current one will be ignored. - * @param timeMs the time in ms to seek to, must be positive. - */ - void setRemoteControlClientPlaybackPosition(int generationId, long timeMs); - /** - * 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, in Rating value); - - /** - * Do not use directly, use instead - * {@link android.media.AudioManager#registerRemoteControlClient(RemoteControlClient)} - */ - int registerRemoteControlClient(in PendingIntent mediaIntent, - in IRemoteControlClient rcClient, in String callingPackageName); - /** - * Do not use directly, use instead - * {@link android.media.AudioManager#unregisterRemoteControlClient(RemoteControlClient)} - */ - oneway void unregisterRemoteControlClient(in PendingIntent mediaIntent, - in IRemoteControlClient rcClient); - - oneway void setPlaybackInfoForRcc(int rccId, int what, int value); - void setPlaybackStateForRcc(int rccId, int state, long timeMs, float speed); - int getRemoteStreamMaxVolume(); - int getRemoteStreamVolume(); - oneway void registerRemoteVolumeObserverForRcc(int rccId, in IRemoteVolumeObserver rvo); void startBluetoothSco(IBinder cb, int targetSdkVersion); + void startBluetoothScoVirtualCall(IBinder cb); void stopBluetoothSco(IBinder cb); void forceVolumeControlStream(int streamType, IBinder cb); @@ -232,7 +186,7 @@ interface IAudioService { int getMasterStreamType(); void setWiredDeviceConnectionState(int device, int state, String name); - int setBluetoothA2dpDeviceConnectionState(in BluetoothDevice device, int state); + int setBluetoothA2dpDeviceConnectionState(in BluetoothDevice device, int state, int profile); AudioRoutesInfo startWatchingRoutes(in IAudioRoutesObserver observer); diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index f258063..22db344 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -782,7 +782,7 @@ final public class MediaCodec { private void postEventFromNative( int what, int arg1, int arg2, Object obj) { - if (mEventHandler != null) { + if (mEventHandler != null && mNotificationCallback != null) { Message msg = mEventHandler.obtainMessage(what, arg1, arg2, obj); mEventHandler.sendMessage(msg); } diff --git a/media/java/android/media/MediaDrm.java b/media/java/android/media/MediaDrm.java index 440653a..6559bc5 100644 --- a/media/java/android/media/MediaDrm.java +++ b/media/java/android/media/MediaDrm.java @@ -181,6 +181,27 @@ public final class MediaDrm { } /** + * Thrown when an unrecoverable failure occurs during a MediaDrm operation. + * Extends java.lang.IllegalStateException with the addition of an error + * code that may be useful in diagnosing the failure. + */ + public static final class MediaDrmStateException extends java.lang.IllegalStateException { + private final int mErrorCode; + + public MediaDrmStateException(int errorCode, String detailMessage) { + super(detailMessage); + mErrorCode = errorCode; + } + + /** + * Retrieve the associated error code + */ + public int getErrorCode() { + return mErrorCode; + } + } + + /** * Register a callback to be invoked when an event occurs * * @param listener the callback that will be run diff --git a/media/java/android/media/MediaFocusControl.java b/media/java/android/media/MediaFocusControl.java index 1c73c05..a4a7c4e 100644 --- a/media/java/android/media/MediaFocusControl.java +++ b/media/java/android/media/MediaFocusControl.java @@ -379,32 +379,11 @@ public class MediaFocusControl implements OnFinished { onReevaluateRemote(); break; - case MSG_RCC_NEW_PLAYBACK_INFO: - onNewPlaybackInfoForRcc(msg.arg1 /* rccId */, msg.arg2 /* key */, - ((Integer)msg.obj).intValue() /* value */); - break; - case MSG_RCC_NEW_VOLUME_OBS: onRegisterVolumeObserverForRcc(msg.arg1 /* rccId */, (IRemoteVolumeObserver)msg.obj /* rvo */); break; - case MSG_RCC_NEW_PLAYBACK_STATE: - onNewPlaybackStateForRcc(msg.arg1 /* rccId */, - msg.arg2 /* state */, - (PlayerRecord.RccPlaybackState)msg.obj /* newState */); - break; - - case MSG_RCC_SEEK_REQUEST: - onSetRemoteControlClientPlaybackPosition( - msg.arg1 /* generationId */, ((Long)msg.obj).longValue() /* timeMs */); - break; - - case MSG_RCC_UPDATE_METADATA: - onUpdateRemoteControlClientMetadata(msg.arg1 /*genId*/, msg.arg2 /*key*/, - (Rating) msg.obj /* value */); - break; - case MSG_RCDISPLAY_INIT_INFO: // msg.obj is guaranteed to be non null onRcDisplayInitInfo((IRemoteControlDisplay)msg.obj /*newRcd*/, @@ -2003,217 +1982,6 @@ public class MediaFocusControl implements OnFinished { } } - protected void setRemoteControlClientPlaybackPosition(int generationId, long timeMs) { - // ignore position change requests if invalid generation ID - synchronized(mPRStack) { - synchronized(mCurrentRcLock) { - if (mCurrentRcClientGen != generationId) { - return; - } - } - } - // discard any unprocessed seek request in the message queue, and replace with latest - sendMsg(mEventHandler, MSG_RCC_SEEK_REQUEST, SENDMSG_REPLACE, generationId /* arg1 */, - 0 /* arg2 ignored*/, new Long(timeMs) /* obj */, 0 /* delay */); - } - - private void onSetRemoteControlClientPlaybackPosition(int generationId, long timeMs) { - if(DEBUG_RC) Log.d(TAG, "onSetRemoteControlClientPlaybackPosition(genId=" + generationId + - ", timeMs=" + timeMs + ")"); - synchronized(mPRStack) { - synchronized(mCurrentRcLock) { - if ((mCurrentRcClient != null) && (mCurrentRcClientGen == generationId)) { - // tell the current client to seek to the requested location - try { - mCurrentRcClient.seekTo(generationId, timeMs); - } catch (RemoteException e) { - Log.e(TAG, "Current valid remote client is dead: "+e); - mCurrentRcClient = null; - } - } - } - } - } - - 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 onUpdateRemoteControlClientMetadata(int genId, int key, Rating value) { - if(DEBUG_RC) Log.d(TAG, "onUpdateRemoteControlClientMetadata(genId=" + genId + - ", what=" + key + ",rating=" + value + ")"); - synchronized(mPRStack) { - synchronized(mCurrentRcLock) { - if ((mCurrentRcClient != null) && (mCurrentRcClientGen == genId)) { - try { - switch (key) { - case MediaMetadataEditor.RATING_KEY_BY_USER: - mCurrentRcClient.updateMetadata(genId, key, value); - break; - default: - Log.e(TAG, "unhandled metadata key " + key + " update for RCC " - + genId); - break; - } - } catch (RemoteException e) { - Log.e(TAG, "Current valid remote client is dead", e); - mCurrentRcClient = null; - } - } - } - } - } - - protected void setPlaybackInfoForRcc(int rccId, int what, int value) { - sendMsg(mEventHandler, MSG_RCC_NEW_PLAYBACK_INFO, SENDMSG_QUEUE, - rccId /* arg1 */, what /* arg2 */, Integer.valueOf(value) /* obj */, 0 /* delay */); - } - - // handler for MSG_RCC_NEW_PLAYBACK_INFO - private void onNewPlaybackInfoForRcc(int rccId, int key, int value) { - if(DEBUG_RC) Log.d(TAG, "onNewPlaybackInfoForRcc(id=" + rccId + - ", what=" + key + ",val=" + value + ")"); - synchronized(mPRStack) { - // iterating from top of stack as playback information changes are more likely - // on entries at the top of the remote control stack - try { - for (int index = mPRStack.size()-1; index >= 0; index--) { - final PlayerRecord prse = mPRStack.elementAt(index); - if (prse.getRccId() == rccId) { - switch (key) { - case RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE: - prse.mPlaybackType = value; - postReevaluateRemote(); - break; - case RemoteControlClient.PLAYBACKINFO_VOLUME: - prse.mPlaybackVolume = value; - synchronized (mMainRemote) { - if (rccId == mMainRemote.mRccId) { - mMainRemote.mVolume = value; - mVolumeController.postHasNewRemotePlaybackInfo(); - } - } - break; - case RemoteControlClient.PLAYBACKINFO_VOLUME_MAX: - prse.mPlaybackVolumeMax = value; - synchronized (mMainRemote) { - if (rccId == mMainRemote.mRccId) { - mMainRemote.mVolumeMax = value; - mVolumeController.postHasNewRemotePlaybackInfo(); - } - } - break; - case RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING: - prse.mPlaybackVolumeHandling = value; - synchronized (mMainRemote) { - if (rccId == mMainRemote.mRccId) { - mMainRemote.mVolumeHandling = value; - mVolumeController.postHasNewRemotePlaybackInfo(); - } - } - break; - case RemoteControlClient.PLAYBACKINFO_USES_STREAM: - prse.mPlaybackStream = value; - break; - default: - Log.e(TAG, "unhandled key " + key + " for RCC " + rccId); - break; - } - return; - } - }//for - } catch (ArrayIndexOutOfBoundsException e) { - // not expected to happen, indicates improper concurrent modification - Log.e(TAG, "Wrong index mPRStack on onNewPlaybackInfoForRcc, lock error? ", e); - } - } - } - - protected void setPlaybackStateForRcc(int rccId, int state, long timeMs, float speed) { - sendMsg(mEventHandler, MSG_RCC_NEW_PLAYBACK_STATE, SENDMSG_QUEUE, - rccId /* arg1 */, state /* arg2 */, - new PlayerRecord.RccPlaybackState(state, timeMs, speed) /* obj */, 0 /* delay */); - } - - private void onNewPlaybackStateForRcc(int rccId, int state, - PlayerRecord.RccPlaybackState newState) { - if(DEBUG_RC) Log.d(TAG, "onNewPlaybackStateForRcc(id=" + rccId + ", state=" + state - + ", time=" + newState.mPositionMs + ", speed=" + newState.mSpeed + ")"); - synchronized(mPRStack) { - if (mPRStack.empty()) { - return; - } - PlayerRecord oldTopPrse = mPRStack.lastElement(); // top of the stack before any changes - PlayerRecord prse = null; - int lastPlayingIndex = mPRStack.size(); - int inStackIndex = -1; - try { - // go through the stack from the top to figure out who's playing, and the position - // of this RemoteControlClient (note that it may not be in the stack) - for (int index = mPRStack.size()-1; index >= 0; index--) { - prse = mPRStack.elementAt(index); - if (prse.getRccId() == rccId) { - inStackIndex = index; - prse.mPlaybackState = newState; - } - if (prse.isPlaybackActive()) { - lastPlayingIndex = index; - } - } - - if (inStackIndex != -1) { - // is in the stack - prse = mPRStack.elementAt(inStackIndex); - synchronized (mMainRemote) { - if (rccId == mMainRemote.mRccId) { - mMainRemoteIsActive = isPlaystateActive(state); - postReevaluateRemote(); - } - } - if (mPRStack.size() > 1) { // no need to remove and add if stack contains only 1 - // remove it from its old location in the stack - mPRStack.removeElementAt(inStackIndex); - if (prse.isPlaybackActive()) { - // and put it at the top - mPRStack.push(prse); - } else { - // and put it after the ones with active playback - if (inStackIndex > lastPlayingIndex) { - mPRStack.add(lastPlayingIndex, prse); - } else { - mPRStack.add(lastPlayingIndex - 1, prse); - } - } - } - - if (oldTopPrse != mPRStack.lastElement()) { - // the top of the stack changed: - final ComponentName target = - mPRStack.lastElement().getMediaButtonReceiver(); - if (target != null) { - // post message to persist the default media button receiver - mEventHandler.sendMessage( mEventHandler.obtainMessage( - MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0, target/*obj*/) ); - } - // reevaluate the display - checkUpdateRemoteControlDisplay_syncPrs(RC_INFO_ALL); - } - } - } catch (ArrayIndexOutOfBoundsException e) { - // not expected to happen, indicates improper concurrent modification or bad index - Log.e(TAG, "Wrong index (inStack=" + inStackIndex + " lastPlaying=" + lastPlayingIndex - + " size=" + mPRStack.size() - + "accessing PlayerRecord stack in onNewPlaybackStateForRcc", e); - } - } - } - - protected void registerRemoteVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) { - sendMsg(mEventHandler, MSG_RCC_NEW_VOLUME_OBS, SENDMSG_QUEUE, - rccId /* arg1 */, 0, rvo /* obj */, 0 /* delay */); - } - // handler for MSG_RCC_NEW_VOLUME_OBS private void onRegisterVolumeObserverForRcc(int rccId, IRemoteVolumeObserver rvo) { synchronized(mPRStack) { diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index 1b92410..d1909bc 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -1649,8 +1649,8 @@ public class MediaPlayer implements SubtitleController.Listener mFormat = MediaFormat.createSubtitleFormat( MEDIA_MIMETYPE_TEXT_SUBRIP, language); } else if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) { - mFormat = MediaFormat.createSubtitleFormat( - MEDIA_MIMETYPE_TEXT_VTT, language); + String mime = in.readString(); + mFormat = MediaFormat.createSubtitleFormat(mime, language); 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()); @@ -1683,12 +1683,40 @@ public class MediaPlayer implements SubtitleController.Listener dest.writeString(getLanguage()); if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) { + dest.writeString(mFormat.getString(MediaFormat.KEY_MIME)); 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)); } } + @Override + public String toString() { + StringBuilder out = new StringBuilder(128); + out.append(getClass().getName()); + out.append('{'); + switch (mTrackType) { + case MEDIA_TRACK_TYPE_VIDEO: + out.append("VIDEO"); + break; + case MEDIA_TRACK_TYPE_AUDIO: + out.append("AUDIO"); + break; + case MEDIA_TRACK_TYPE_TIMEDTEXT: + out.append("TIMEDTEXT"); + break; + case MEDIA_TRACK_TYPE_SUBTITLE: + out.append("SUBTITLE"); + break; + default: + out.append("UNKNOWN"); + break; + } + out.append(", " + mFormat.toString()); + out.append("}"); + return out.toString(); + } + /** * Used to read a TrackInfo from a Parcel. */ @@ -1757,6 +1785,12 @@ public class MediaPlayer implements SubtitleController.Listener */ public static final String MEDIA_MIMETYPE_TEXT_VTT = "text/vtt"; + /** + * MIME type for CEA-608 closed caption data. + * @hide + */ + public static final String MEDIA_MIMETYPE_TEXT_CEA_608 = "text/cea-608"; + /* * A helper function to check if the mime type is supported by media framework. */ @@ -1792,16 +1826,7 @@ public class MediaPlayer implements SubtitleController.Listener } SubtitleTrack track = mInbandSubtitleTracks[index]; if (track != null) { - try { - long runID = data.getStartTimeUs() + 1; - // TODO: move conversion into track - track.onData(new String(data.getData(), "UTF-8"), true /* eos */, runID); - track.setRunDiscardTimeMs( - runID, - (data.getStartTimeUs() + data.getDurationUs()) / 1000); - } catch (java.io.UnsupportedEncodingException e) { - Log.w(TAG, "subtitle data for track " + index + " is not UTF-8 encoded: " + e); - } + track.onData(data); } } }; @@ -1872,7 +1897,7 @@ public class MediaPlayer implements SubtitleController.Listener } scanner.close(); mOutOfBandSubtitleTracks.add(track); - track.onData(contents, true /* eos */, ~0 /* runID: keep forever */); + track.onData(contents.getBytes(), true /* eos */, ~0 /* runID: keep forever */); return MEDIA_INFO_EXTERNAL_METADATA_UPDATE; } @@ -2310,7 +2335,9 @@ public class MediaPlayer implements SubtitleController.Listener case MEDIA_INFO_EXTERNAL_METADATA_UPDATE: msg.arg1 = MEDIA_INFO_METADATA_UPDATE; // update default track selection - mSubtitleController.selectDefaultTrack(); + if (mSubtitleController != null) { + mSubtitleController.selectDefaultTrack(); + } break; } diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index 1da0215..3336694 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -29,7 +29,6 @@ import android.hardware.display.DisplayManager; import android.hardware.display.WifiDisplay; import android.hardware.display.WifiDisplayStatus; import android.media.session.MediaSession; -import android.media.session.RemoteVolumeProvider; import android.os.Handler; import android.os.IBinder; import android.os.Process; @@ -60,7 +59,6 @@ import java.util.concurrent.CopyOnWriteArrayList; public class MediaRouter { private static final String TAG = "MediaRouter"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final boolean USE_SESSIONS = true; static class Static implements DisplayManager.DisplayListener { final Context mAppContext; @@ -2104,11 +2102,7 @@ public class MediaRouter { public void setPlaybackType(int type) { if (mPlaybackType != type) { mPlaybackType = type; - if (USE_SESSIONS) { - configureSessionVolume(); - } else { - setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type); - } + configureSessionVolume(); } } @@ -2121,12 +2115,7 @@ public class MediaRouter { public void setVolumeHandling(int volumeHandling) { if (mVolumeHandling != volumeHandling) { mVolumeHandling = volumeHandling; - if (USE_SESSIONS) { - configureSessionVolume(); - } else { - setPlaybackInfoOnRcc( - RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling); - } + configureSessionVolume(); } } @@ -2139,12 +2128,8 @@ public class MediaRouter { volume = Math.max(0, Math.min(volume, getVolumeMax())); if (mVolume != volume) { mVolume = volume; - if (USE_SESSIONS) { - if (mSvp != null) { - mSvp.notifyVolumeChanged(); - } - } else { - setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume); + if (mSvp != null) { + mSvp.notifyVolumeChanged(); } dispatchRouteVolumeChanged(this); if (mGroup != null) { @@ -2184,11 +2169,7 @@ public class MediaRouter { public void setVolumeMax(int volumeMax) { if (mVolumeMax != volumeMax) { mVolumeMax = volumeMax; - if (USE_SESSIONS) { - configureSessionVolume(); - } else { - setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax); - } + configureSessionVolume(); } } @@ -2199,40 +2180,12 @@ public class MediaRouter { public void setPlaybackStream(int stream) { if (mPlaybackStream != stream) { mPlaybackStream = stream; - if (USE_SESSIONS) { - configureSessionVolume(); - } else { - setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream); - } + configureSessionVolume(); } } private void updatePlaybackInfoOnRcc() { - if (USE_SESSIONS) { - configureSessionVolume(); - } else { - if ((mRcc != null) - && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) { - mRcc.setPlaybackInformation( - RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax); - mRcc.setPlaybackInformation( - RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume); - mRcc.setPlaybackInformation( - RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling); - mRcc.setPlaybackInformation( - RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream); - mRcc.setPlaybackInformation( - RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType); - // let AudioService know whom to call when remote volume - // needs to be updated - try { - sStatic.mAudioService.registerRemoteVolumeObserverForRcc( - mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */); - } catch (RemoteException e) { - Log.e(TAG, "Error registering remote volume observer", e); - } - } - } + configureSessionVolume(); } private void configureSessionVolume() { @@ -2250,10 +2203,10 @@ public class MediaRouter { return; } if (mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE) { - int volumeControl = RemoteVolumeProvider.VOLUME_CONTROL_FIXED; + int volumeControl = VolumeProvider.VOLUME_CONTROL_FIXED; switch (mVolumeHandling) { case RemoteControlClient.PLAYBACK_VOLUME_VARIABLE: - volumeControl = RemoteVolumeProvider.VOLUME_CONTROL_ABSOLUTE; + volumeControl = VolumeProvider.VOLUME_CONTROL_ABSOLUTE; break; case RemoteControlClient.PLAYBACK_VOLUME_FIXED: default: @@ -2272,13 +2225,7 @@ public class MediaRouter { } } - private void setPlaybackInfoOnRcc(int what, int value) { - if (mRcc != null) { - mRcc.setPlaybackInformation(what, value); - } - } - - class SessionVolumeProvider extends RemoteVolumeProvider { + class SessionVolumeProvider extends VolumeProvider { public SessionVolumeProvider(int volumeControl, int maxVolume) { super(volumeControl, maxVolume); diff --git a/media/java/android/media/MiniThumbFile.java b/media/java/android/media/MiniThumbFile.java index 23c3652..664308c 100644 --- a/media/java/android/media/MiniThumbFile.java +++ b/media/java/android/media/MiniThumbFile.java @@ -248,7 +248,8 @@ public class MiniThumbFile { long magic = mBuffer.getLong(); int length = mBuffer.getInt(); - if (size >= 1 + 8 + 4 + length && data.length >= length) { + if (size >= 1 + 8 + 4 + length && length != 0 && magic != 0 && flag == 1 && + data.length >= length) { mBuffer.get(data, 0, length); return data; } diff --git a/media/java/android/media/PlayerRecord.java b/media/java/android/media/PlayerRecord.java index f9708c3..664ddcf 100644 --- a/media/java/android/media/PlayerRecord.java +++ b/media/java/android/media/PlayerRecord.java @@ -56,7 +56,7 @@ class PlayerRecord implements DeathRecipient { */ final private ComponentName mReceiverComponent; - private int mRccId = RemoteControlClient.RCSE_ID_UNREGISTERED; + private int mRccId = -1; /** * A non-null token implies this record tracks a "live" player whose death is being monitored. diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java index 0caea5f..73bc61a 100644 --- a/media/java/android/media/RemoteControlClient.java +++ b/media/java/android/media/RemoteControlClient.java @@ -561,6 +561,8 @@ public class RemoteControlClient return; } synchronized (mCacheLock) { + // Still build the old metadata so when creating a new editor + // you get the expected values. // assign the edited data mMetadata = new Bundle(mEditorMetadata); // add the information about editable keys @@ -570,16 +572,6 @@ public class RemoteControlClient } mOriginalArtwork = mEditorArtwork; mEditorArtwork = null; - if (mMetadataChanged & mArtworkChanged) { - // send to remote control display if conditions are met - sendMetadataWithArtwork_syncCacheLock(null, 0, 0); - } else if (mMetadataChanged) { - // send to remote control display if conditions are met - sendMetadata_syncCacheLock(null); - } else if (mArtworkChanged) { - // send to remote control display if conditions are met - sendArtwork_syncCacheLock(null, 0, 0); - } // USE_SESSIONS if (mSession != null && mMetadataBuilder != null) { @@ -687,14 +679,6 @@ public class RemoteControlClient // keep track of when the state change occurred mPlaybackStateChangeTimeMs = SystemClock.elapsedRealtime(); - // send to remote control display if conditions are met - sendPlaybackState_syncCacheLock(null); - // update AudioService - sendAudioServiceNewPlaybackState_syncCacheLock(); - - // handle automatic playback position refreshes - initiateCheckForDrift_syncCacheLock(); - // USE_SESSIONS if (mSession != null) { int pbState = PlaybackState.getStateFromRccState(state); @@ -707,29 +691,7 @@ public class RemoteControlClient } } - private void initiateCheckForDrift_syncCacheLock() { - if (mEventHandler == null) { - return; - } - mEventHandler.removeMessages(MSG_POSITION_DRIFT_CHECK); - if (!mNeedsPositionSync) { - return; - } - if (mPlaybackPositionMs < 0) { - // the current playback state has no known playback position, it's no use - // trying to see if there is any drift at this point - // (this also bypasses this mechanism for older apps that use the old - // setPlaybackState(int) API) - return; - } - if (playbackPositionShouldMove(mPlaybackState)) { - // playback position moving, schedule next position drift check - mEventHandler.sendMessageDelayed( - mEventHandler.obtainMessage(MSG_POSITION_DRIFT_CHECK), - getCheckPeriodFromSpeed(mPlaybackSpeed)); - } - } - + // TODO investigate if we still need position drift checking private void onPositionDriftCheck() { if (DEBUG) { Log.d(TAG, "onPositionDriftCheck()"); } synchronized(mCacheLock) { @@ -781,9 +743,6 @@ public class RemoteControlClient // store locally mTransportControlFlags = transportControlFlags; - // send to remote control display if conditions are met - sendTransportControlInfo_syncCacheLock(null); - // USE_SESSIONS if (mSession != null) { mSessionPlaybackState.setActions(PlaybackState @@ -866,17 +825,7 @@ public class RemoteControlClient */ public void setPlaybackPositionUpdateListener(OnPlaybackPositionUpdateListener l) { synchronized(mCacheLock) { - int oldCapa = mPlaybackPositionCapabilities; - if (l != null) { - mPlaybackPositionCapabilities |= MEDIA_POSITION_WRITABLE; - } else { - mPlaybackPositionCapabilities &= ~MEDIA_POSITION_WRITABLE; - } mPositionUpdateListener = l; - if (oldCapa != mPlaybackPositionCapabilities) { - // tell RCDs that this RCC's playback position capabilities have changed - sendTransportControlInfo_syncCacheLock(null); - } } } @@ -888,17 +837,7 @@ public class RemoteControlClient */ public void setOnGetPlaybackPositionListener(OnGetPlaybackPositionListener l) { synchronized(mCacheLock) { - int oldCapa = mPlaybackPositionCapabilities; - if (l != null) { - mPlaybackPositionCapabilities |= MEDIA_POSITION_READABLE; - } else { - mPlaybackPositionCapabilities &= ~MEDIA_POSITION_READABLE; - } mPositionProvider = l; - if (oldCapa != mPlaybackPositionCapabilities) { - // tell RCDs that this RCC's playback position capabilities have changed - sendTransportControlInfo_syncCacheLock(null); - } if ((mPositionProvider != null) && (mEventHandler != null) && playbackPositionShouldMove(mPlaybackState)) { // playback position is already moving, but now we have a position provider, @@ -925,124 +864,12 @@ public class RemoteControlClient */ public static int MEDIA_POSITION_WRITABLE = 1 << 1; - private int mPlaybackPositionCapabilities = 0; - /** @hide */ public final static int DEFAULT_PLAYBACK_VOLUME_HANDLING = PLAYBACK_VOLUME_VARIABLE; /** @hide */ // hard-coded to the same number of steps as AudioService.MAX_STREAM_VOLUME[STREAM_MUSIC] public final static int DEFAULT_PLAYBACK_VOLUME = 15; - private int mPlaybackType = PLAYBACK_TYPE_LOCAL; - private int mPlaybackVolumeMax = DEFAULT_PLAYBACK_VOLUME; - private int mPlaybackVolume = DEFAULT_PLAYBACK_VOLUME; - private int mPlaybackVolumeHandling = DEFAULT_PLAYBACK_VOLUME_HANDLING; - private int mPlaybackStream = AudioManager.STREAM_MUSIC; - - /** - * @hide - * Set information describing information related to the playback of media so the system - * can implement additional behavior to handle non-local playback usecases. - * @param what a key to specify the type of information to set. Valid keys are - * {@link #PLAYBACKINFO_PLAYBACK_TYPE}, - * {@link #PLAYBACKINFO_USES_STREAM}, - * {@link #PLAYBACKINFO_VOLUME}, - * {@link #PLAYBACKINFO_VOLUME_MAX}, - * and {@link #PLAYBACKINFO_VOLUME_HANDLING}. - * @param value the value for the supplied information to set. - */ - public void setPlaybackInformation(int what, int value) { - synchronized(mCacheLock) { - switch (what) { - case PLAYBACKINFO_PLAYBACK_TYPE: - if ((value >= PLAYBACK_TYPE_MIN) && (value <= PLAYBACK_TYPE_MAX)) { - if (mPlaybackType != value) { - mPlaybackType = value; - sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value); - } - } else { - Log.w(TAG, "using invalid value for PLAYBACKINFO_PLAYBACK_TYPE"); - } - break; - case PLAYBACKINFO_VOLUME: - if ((value > -1) && (value <= mPlaybackVolumeMax)) { - if (mPlaybackVolume != value) { - mPlaybackVolume = value; - sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value); - } - } else { - Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME"); - } - break; - case PLAYBACKINFO_VOLUME_MAX: - if (value > 0) { - if (mPlaybackVolumeMax != value) { - mPlaybackVolumeMax = value; - sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value); - } - } else { - Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME_MAX"); - } - break; - case PLAYBACKINFO_USES_STREAM: - if ((value >= 0) && (value < AudioSystem.getNumStreamTypes())) { - mPlaybackStream = value; - } else { - Log.w(TAG, "using invalid value for PLAYBACKINFO_USES_STREAM"); - } - break; - case PLAYBACKINFO_VOLUME_HANDLING: - if ((value >= PLAYBACK_VOLUME_FIXED) && (value <= PLAYBACK_VOLUME_VARIABLE)) { - if (mPlaybackVolumeHandling != value) { - mPlaybackVolumeHandling = value; - sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value); - } - } else { - Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME_HANDLING"); - } - break; - default: - // not throwing an exception or returning an error if more keys are to be - // supported in the future - Log.w(TAG, "setPlaybackInformation() ignoring unknown key " + what); - break; - } - } - } - - /** - * @hide - * Return playback information represented as an integer value. - * @param what a key to specify the type of information to retrieve. Valid keys are - * {@link #PLAYBACKINFO_PLAYBACK_TYPE}, - * {@link #PLAYBACKINFO_USES_STREAM}, - * {@link #PLAYBACKINFO_VOLUME}, - * {@link #PLAYBACKINFO_VOLUME_MAX}, - * and {@link #PLAYBACKINFO_VOLUME_HANDLING}. - * @return the current value for the given information type, or - * {@link #PLAYBACKINFO_INVALID_VALUE} if an error occurred or the request is invalid, or - * the value is unknown. - */ - public int getIntPlaybackInformation(int what) { - synchronized(mCacheLock) { - switch (what) { - case PLAYBACKINFO_PLAYBACK_TYPE: - return mPlaybackType; - case PLAYBACKINFO_VOLUME: - return mPlaybackVolume; - case PLAYBACKINFO_VOLUME_MAX: - return mPlaybackVolumeMax; - case PLAYBACKINFO_USES_STREAM: - return mPlaybackStream; - case PLAYBACKINFO_VOLUME_HANDLING: - return mPlaybackVolumeHandling; - default: - Log.e(TAG, "getIntPlaybackInformation() unknown key " + what); - return PLAYBACKINFO_INVALID_VALUE; - } - } - } - /** * Lock for all cached data */ @@ -1102,13 +929,6 @@ public class RemoteControlClient * The current remote control client generation ID across the system, as known by this object */ private int mCurrentClientGenId = -1; - /** - * The remote control client generation ID, the last time it was told it was the current RC. - * If (mCurrentClientGenId == mInternalClientGenId) is true, it means that this remote control - * client is the "focused" one, and that whenever this client's info is updated, it needs to - * send it to the known IRemoteControlDisplay interfaces. - */ - private int mInternalClientGenId = -2; /** * The media button intent description associated with this remote control client @@ -1134,186 +954,18 @@ public class RemoteControlClient private MediaMetadata mMediaMetadata; /** - * A class to encapsulate all the information about a remote control display. - * A RemoteControlClient's metadata and state may be displayed on multiple IRemoteControlDisplay - */ - private class DisplayInfoForClient { - /** may never be null */ - private IRemoteControlDisplay mRcDisplay; - private int mArtworkExpectedWidth; - private int mArtworkExpectedHeight; - private boolean mWantsPositionSync = false; - private boolean mEnabled = true; - - DisplayInfoForClient(IRemoteControlDisplay rcd, int w, int h) { - mRcDisplay = rcd; - mArtworkExpectedWidth = w; - mArtworkExpectedHeight = h; - } - } - - /** - * The list of remote control displays to which this client will send information. - * Accessed and modified synchronized on mCacheLock - */ - private ArrayList<DisplayInfoForClient> mRcDisplays = new ArrayList<DisplayInfoForClient>(1); - - /** * @hide * Accessor to media button intent description (includes target component) */ public PendingIntent getRcMediaIntent() { return mRcMediaIntent; } - /** - * @hide - * Accessor to IRemoteControlClient - */ - public IRemoteControlClient getIRemoteControlClient() { - return mIRCC; - } - - /** - * The IRemoteControlClient implementation - */ - private final IRemoteControlClient mIRCC = new IRemoteControlClient.Stub() { - - //TODO change name to informationRequestForAllDisplays() - public void onInformationRequested(int generationId, int infoFlags) { - // only post messages, we can't block here - if (mEventHandler != null) { - // signal new client - mEventHandler.removeMessages(MSG_NEW_INTERNAL_CLIENT_GEN); - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_NEW_INTERNAL_CLIENT_GEN, - /*arg1*/ generationId, /*arg2, ignored*/ 0)); - // send the information - mEventHandler.removeMessages(MSG_REQUEST_PLAYBACK_STATE); - mEventHandler.removeMessages(MSG_REQUEST_METADATA); - mEventHandler.removeMessages(MSG_REQUEST_TRANSPORTCONTROL); - mEventHandler.removeMessages(MSG_REQUEST_ARTWORK); - mEventHandler.removeMessages(MSG_REQUEST_METADATA_ARTWORK); - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_REQUEST_PLAYBACK_STATE, null)); - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_REQUEST_TRANSPORTCONTROL, null)); - mEventHandler.sendMessage(mEventHandler.obtainMessage(MSG_REQUEST_METADATA_ARTWORK, - 0, 0, null)); - } - } - - public void informationRequestForDisplay(IRemoteControlDisplay rcd, int w, int h) { - // only post messages, we can't block here - if (mEventHandler != null) { - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_REQUEST_TRANSPORTCONTROL, rcd)); - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_REQUEST_PLAYBACK_STATE, rcd)); - if ((w > 0) && (h > 0)) { - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_REQUEST_METADATA_ARTWORK, w, h, rcd)); - } else { - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_REQUEST_METADATA, rcd)); - } - } - } - - public void setCurrentClientGenerationId(int clientGeneration) { - // only post messages, we can't block here - if (mEventHandler != null) { - mEventHandler.removeMessages(MSG_NEW_CURRENT_CLIENT_GEN); - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_NEW_CURRENT_CLIENT_GEN, clientGeneration, 0/*ignored*/)); - } - } - - public void plugRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) { - // only post messages, we can't block here - if ((mEventHandler != null) && (rcd != null)) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_PLUG_DISPLAY, w, h, rcd)); - } - } - - public void unplugRemoteControlDisplay(IRemoteControlDisplay rcd) { - // only post messages, we can't block here - if ((mEventHandler != null) && (rcd != null)) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_UNPLUG_DISPLAY, rcd)); - } - } - - public void setBitmapSizeForDisplay(IRemoteControlDisplay rcd, int w, int h) { - // only post messages, we can't block here - if ((mEventHandler != null) && (rcd != null)) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_UPDATE_DISPLAY_ARTWORK_SIZE, w, h, rcd)); - } - } - - public void setWantsSyncForDisplay(IRemoteControlDisplay rcd, boolean wantsSync) { - // only post messages, we can't block here - if ((mEventHandler != null) && (rcd != null)) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_DISPLAY_WANTS_POS_SYNC, wantsSync ? 1 : 0, 0/*arg2 ignored*/, rcd)); - } - } - - public void enableRemoteControlDisplay(IRemoteControlDisplay rcd, boolean enabled) { - // only post messages, we can't block here - if ((mEventHandler != null) && (rcd != null)) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_DISPLAY_ENABLE, enabled ? 1 : 0, 0/*arg2 ignored*/, rcd)); - } - } - - public void seekTo(int generationId, long timeMs) { - // only post messages, we can't block here - if (mEventHandler != null) { - mEventHandler.removeMessages(MSG_SEEK_TO); - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_SEEK_TO, generationId /* arg1 */, 0 /* arg2, ignored */, - new Long(timeMs))); - } - } - - public void updateMetadata(int generationId, int key, Rating value) { - // only post messages, we can't block here - if (mEventHandler != null) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_UPDATE_METADATA, generationId /* arg1 */, key /* arg2*/, value)); - } - } - }; /** * @hide * Default value for the unique identifier */ public final static int RCSE_ID_UNREGISTERED = -1; - /** - * Unique identifier of the RemoteControlStackEntry in AudioService with which - * this RemoteControlClient is associated. - */ - private int mRcseId = RCSE_ID_UNREGISTERED; - /** - * @hide - * To be only used by AudioManager after it has received the unique id from - * IAudioService.registerRemoteControlClient() - * @param id the unique identifier of the RemoteControlStackEntry in AudioService with which - * this RemoteControlClient is associated. - */ - public void setRcseId(int id) { - mRcseId = id; - } - - /** - * @hide - */ - public int getRcseId() { - return mRcseId; - } // USE_SESSIONS private MediaSession.TransportControlsCallback mTransportListener @@ -1327,31 +979,13 @@ public class RemoteControlClient @Override public void onSetRating(Rating rating) { if ((mTransportControlFlags & FLAG_KEY_MEDIA_RATING) != 0) { - if (mEventHandler != null) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - MSG_UPDATE_METADATA, mCurrentClientGenId, - MetadataEditor.RATING_KEY_BY_USER, rating)); - } + onUpdateMetadata(mCurrentClientGenId, MetadataEditor.RATING_KEY_BY_USER, rating); } } }; private EventHandler mEventHandler; - private final static int MSG_REQUEST_PLAYBACK_STATE = 1; - private final static int MSG_REQUEST_METADATA = 2; - private final static int MSG_REQUEST_TRANSPORTCONTROL = 3; - private final static int MSG_REQUEST_ARTWORK = 4; - private final static int MSG_NEW_INTERNAL_CLIENT_GEN = 5; - private final static int MSG_NEW_CURRENT_CLIENT_GEN = 6; - private final static int MSG_PLUG_DISPLAY = 7; - private final static int MSG_UNPLUG_DISPLAY = 8; - private final static int MSG_UPDATE_DISPLAY_ARTWORK_SIZE = 9; - private final static int MSG_SEEK_TO = 10; private final static int MSG_POSITION_DRIFT_CHECK = 11; - private final static int MSG_DISPLAY_WANTS_POS_SYNC = 12; - private final static int MSG_UPDATE_METADATA = 13; - private final static int MSG_REQUEST_METADATA_ARTWORK = 14; - private final static int MSG_DISPLAY_ENABLE = 15; private class EventHandler extends Handler { public EventHandler(RemoteControlClient rcc, Looper looper) { @@ -1361,63 +995,9 @@ public class RemoteControlClient @Override public void handleMessage(Message msg) { switch(msg.what) { - case MSG_REQUEST_PLAYBACK_STATE: - synchronized (mCacheLock) { - sendPlaybackState_syncCacheLock((IRemoteControlDisplay)msg.obj); - } - break; - case MSG_REQUEST_METADATA: - synchronized (mCacheLock) { - sendMetadata_syncCacheLock((IRemoteControlDisplay)msg.obj); - } - break; - case MSG_REQUEST_TRANSPORTCONTROL: - synchronized (mCacheLock) { - sendTransportControlInfo_syncCacheLock((IRemoteControlDisplay)msg.obj); - } - break; - case MSG_REQUEST_ARTWORK: - synchronized (mCacheLock) { - sendArtwork_syncCacheLock((IRemoteControlDisplay)msg.obj, - msg.arg1, msg.arg2); - } - break; - case MSG_REQUEST_METADATA_ARTWORK: - synchronized (mCacheLock) { - sendMetadataWithArtwork_syncCacheLock((IRemoteControlDisplay)msg.obj, - msg.arg1, msg.arg2); - } - break; - case MSG_NEW_INTERNAL_CLIENT_GEN: - onNewInternalClientGen(msg.arg1); - break; - case MSG_NEW_CURRENT_CLIENT_GEN: - onNewCurrentClientGen(msg.arg1); - break; - case MSG_PLUG_DISPLAY: - onPlugDisplay((IRemoteControlDisplay)msg.obj, msg.arg1, msg.arg2); - break; - case MSG_UNPLUG_DISPLAY: - onUnplugDisplay((IRemoteControlDisplay)msg.obj); - break; - case MSG_UPDATE_DISPLAY_ARTWORK_SIZE: - onUpdateDisplayArtworkSize((IRemoteControlDisplay)msg.obj, msg.arg1, msg.arg2); - break; - case MSG_SEEK_TO: - onSeekTo(msg.arg1, ((Long)msg.obj).longValue()); - break; case MSG_POSITION_DRIFT_CHECK: onPositionDriftCheck(); break; - case MSG_DISPLAY_WANTS_POS_SYNC: - onDisplayWantsSync((IRemoteControlDisplay)msg.obj, msg.arg1 == 1); - break; - case MSG_UPDATE_METADATA: - onUpdateMetadata(msg.arg1, msg.arg2, msg.obj); - break; - case MSG_DISPLAY_ENABLE: - onDisplayEnable((IRemoteControlDisplay)msg.obj, msg.arg1 == 1); - break; default: Log.e(TAG, "Unknown event " + msg.what + " in RemoteControlClient handler"); } @@ -1425,346 +1005,8 @@ public class RemoteControlClient } //=========================================================== - // Communication with the IRemoteControlDisplay (the displays known to the system) - - private void sendPlaybackState_syncCacheLock(IRemoteControlDisplay target) { - if (mCurrentClientGenId == mInternalClientGenId) { - if (target != null) { - try { - target.setPlaybackState(mInternalClientGenId, - mPlaybackState, mPlaybackStateChangeTimeMs, mPlaybackPositionMs, - mPlaybackSpeed); - } catch (RemoteException e) { - Log.e(TAG, "Error in setPlaybackState() for dead display " + target, e); - } - return; - } - // target == null implies all displays must be updated - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - if (di.mEnabled) { - try { - di.mRcDisplay.setPlaybackState(mInternalClientGenId, - mPlaybackState, mPlaybackStateChangeTimeMs, mPlaybackPositionMs, - mPlaybackSpeed); - } catch (RemoteException e) { - Log.e(TAG, "Error in setPlaybackState(), dead display " + di.mRcDisplay, e); - displayIterator.remove(); - } - } - } - } - } - - private void sendMetadata_syncCacheLock(IRemoteControlDisplay target) { - if (mCurrentClientGenId == mInternalClientGenId) { - if (target != null) { - try { - target.setMetadata(mInternalClientGenId, mMetadata); - } catch (RemoteException e) { - Log.e(TAG, "Error in setMetadata() for dead display " + target, e); - } - return; - } - // target == null implies all displays must be updated - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - if (di.mEnabled) { - try { - di.mRcDisplay.setMetadata(mInternalClientGenId, mMetadata); - } catch (RemoteException e) { - Log.e(TAG, "Error in setMetadata(), dead display " + di.mRcDisplay, e); - displayIterator.remove(); - } - } - } - } - } - - private void sendTransportControlInfo_syncCacheLock(IRemoteControlDisplay target) { - if (mCurrentClientGenId == mInternalClientGenId) { - if (target != null) { - try { - target.setTransportControlInfo(mInternalClientGenId, - mTransportControlFlags, mPlaybackPositionCapabilities); - } catch (RemoteException e) { - Log.e(TAG, "Error in setTransportControlFlags() for dead display " + target, - e); - } - return; - } - // target == null implies all displays must be updated - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - if (di.mEnabled) { - try { - di.mRcDisplay.setTransportControlInfo(mInternalClientGenId, - mTransportControlFlags, mPlaybackPositionCapabilities); - } catch (RemoteException e) { - Log.e(TAG, "Error in setTransportControlFlags(), dead display " + di.mRcDisplay, - e); - displayIterator.remove(); - } - } - } - } - } - - private void sendArtwork_syncCacheLock(IRemoteControlDisplay target, int w, int h) { - // FIXME modify to cache all requested sizes? - if (mCurrentClientGenId == mInternalClientGenId) { - if (target != null) { - final DisplayInfoForClient di = new DisplayInfoForClient(target, w, h); - sendArtworkToDisplay(di); - return; - } - // target == null implies all displays must be updated - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - if (!sendArtworkToDisplay(displayIterator.next())) { - displayIterator.remove(); - } - } - } - } - - /** - * Send artwork to an IRemoteControlDisplay. - * @param di encapsulates the IRemoteControlDisplay that will receive the artwork, and its - * dimension requirements. - * @return false if there was an error communicating with the IRemoteControlDisplay. - */ - private boolean sendArtworkToDisplay(DisplayInfoForClient di) { - if ((di.mArtworkExpectedWidth > 0) && (di.mArtworkExpectedHeight > 0)) { - Bitmap artwork = scaleBitmapIfTooBig(mOriginalArtwork, - di.mArtworkExpectedWidth, di.mArtworkExpectedHeight); - try { - di.mRcDisplay.setArtwork(mInternalClientGenId, artwork); - } catch (RemoteException e) { - Log.e(TAG, "Error in sendArtworkToDisplay(), dead display " + di.mRcDisplay, e); - return false; - } - } - return true; - } - - private void sendMetadataWithArtwork_syncCacheLock(IRemoteControlDisplay target, int w, int h) { - // FIXME modify to cache all requested sizes? - if (mCurrentClientGenId == mInternalClientGenId) { - if (target != null) { - try { - if ((w > 0) && (h > 0)) { - Bitmap artwork = scaleBitmapIfTooBig(mOriginalArtwork, w, h); - target.setAllMetadata(mInternalClientGenId, mMetadata, artwork); - } else { - target.setMetadata(mInternalClientGenId, mMetadata); - } - } catch (RemoteException e) { - Log.e(TAG, "Error in set(All)Metadata() for dead display " + target, e); - } - return; - } - // target == null implies all displays must be updated - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - try { - if (di.mEnabled) { - if ((di.mArtworkExpectedWidth > 0) && (di.mArtworkExpectedHeight > 0)) { - Bitmap artwork = scaleBitmapIfTooBig(mOriginalArtwork, - di.mArtworkExpectedWidth, di.mArtworkExpectedHeight); - di.mRcDisplay.setAllMetadata(mInternalClientGenId, mMetadata, artwork); - } else { - di.mRcDisplay.setMetadata(mInternalClientGenId, mMetadata); - } - } - } catch (RemoteException e) { - Log.e(TAG, "Error when setting metadata, dead display " + di.mRcDisplay, e); - displayIterator.remove(); - } - } - } - } - - //=========================================================== - // Communication with AudioService - - private static IAudioService sService; - - private static IAudioService getService() - { - if (sService != null) { - return sService; - } - IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); - sService = IAudioService.Stub.asInterface(b); - return sService; - } - - private void sendAudioServiceNewPlaybackInfo_syncCacheLock(int what, int value) { - if (mRcseId == RCSE_ID_UNREGISTERED) { - return; - } - //Log.d(TAG, "sending to AudioService key=" + what + ", value=" + value); - IAudioService service = getService(); - try { - service.setPlaybackInfoForRcc(mRcseId, what, value); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in setPlaybackInfoForRcc", e); - } - } - - private void sendAudioServiceNewPlaybackState_syncCacheLock() { - if (mRcseId == RCSE_ID_UNREGISTERED) { - return; - } - IAudioService service = getService(); - try { - service.setPlaybackStateForRcc(mRcseId, - mPlaybackState, mPlaybackPositionMs, mPlaybackSpeed); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in setPlaybackStateForRcc", e); - } - } - - //=========================================================== // Message handlers - private void onNewInternalClientGen(int clientGeneration) { - synchronized (mCacheLock) { - // this remote control client is told it is the "focused" one: - // it implies that now (mCurrentClientGenId == mInternalClientGenId) is true - mInternalClientGenId = clientGeneration; - } - } - - private void onNewCurrentClientGen(int clientGeneration) { - synchronized (mCacheLock) { - mCurrentClientGenId = clientGeneration; - } - } - - /** pre-condition rcd != null */ - private void onPlugDisplay(IRemoteControlDisplay rcd, int w, int h) { - synchronized(mCacheLock) { - // do we have this display already? - boolean displayKnown = false; - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext() && !displayKnown) { - final DisplayInfoForClient di = displayIterator.next(); - displayKnown = di.mRcDisplay.asBinder().equals(rcd.asBinder()); - if (displayKnown) { - // this display was known but the change in artwork size will cause the - // artwork to be refreshed - if ((di.mArtworkExpectedWidth != w) || (di.mArtworkExpectedHeight != h)) { - di.mArtworkExpectedWidth = w; - di.mArtworkExpectedHeight = h; - if (!sendArtworkToDisplay(di)) { - displayIterator.remove(); - } - } - } - } - if (!displayKnown) { - mRcDisplays.add(new DisplayInfoForClient(rcd, w, h)); - } - } - } - - /** pre-condition rcd != null */ - private void onUnplugDisplay(IRemoteControlDisplay rcd) { - synchronized(mCacheLock) { - Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) { - displayIterator.remove(); - break; - } - } - // list of RCDs has changed, reevaluate whether position check is still needed - boolean oldNeedsPositionSync = mNeedsPositionSync; - boolean newNeedsPositionSync = false; - displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - if (di.mWantsPositionSync) { - newNeedsPositionSync = true; - break; - } - } - mNeedsPositionSync = newNeedsPositionSync; - if (oldNeedsPositionSync != mNeedsPositionSync) { - // update needed? - initiateCheckForDrift_syncCacheLock(); - } - } - } - - /** pre-condition rcd != null */ - private void onUpdateDisplayArtworkSize(IRemoteControlDisplay rcd, int w, int h) { - synchronized(mCacheLock) { - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - if (di.mRcDisplay.asBinder().equals(rcd.asBinder()) && - ((di.mArtworkExpectedWidth != w) || (di.mArtworkExpectedHeight != h))) { - di.mArtworkExpectedWidth = w; - di.mArtworkExpectedHeight = h; - if (di.mEnabled) { - if (!sendArtworkToDisplay(di)) { - displayIterator.remove(); - } - } - break; - } - } - } - } - - /** pre-condition rcd != null */ - private void onDisplayWantsSync(IRemoteControlDisplay rcd, boolean wantsSync) { - synchronized(mCacheLock) { - boolean oldNeedsPositionSync = mNeedsPositionSync; - boolean newNeedsPositionSync = false; - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - // go through the list of RCDs and for each entry, check both whether this is the RCD - // that gets upated, and whether the list has one entry that wants position sync - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - if (di.mEnabled) { - if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) { - di.mWantsPositionSync = wantsSync; - } - if (di.mWantsPositionSync) { - newNeedsPositionSync = true; - } - } - } - mNeedsPositionSync = newNeedsPositionSync; - if (oldNeedsPositionSync != mNeedsPositionSync) { - // update needed? - initiateCheckForDrift_syncCacheLock(); - } - } - } - - /** pre-condition rcd != null */ - private void onDisplayEnable(IRemoteControlDisplay rcd, boolean enable) { - synchronized(mCacheLock) { - final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator(); - while (displayIterator.hasNext()) { - final DisplayInfoForClient di = displayIterator.next(); - if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) { - di.mEnabled = enable; - } - } - } - } - private void onSeekTo(int generationId, long timeMs) { synchronized (mCacheLock) { if ((mCurrentClientGenId == generationId) && (mPositionUpdateListener != null)) { @@ -1785,42 +1027,6 @@ public class RemoteControlClient // Internal utilities /** - * Scale a bitmap to fit the smallest dimension by uniformly scaling the incoming bitmap. - * If the bitmap fits, then do nothing and return the original. - * - * @param bitmap - * @param maxWidth - * @param maxHeight - * @return - */ - - private Bitmap scaleBitmapIfTooBig(Bitmap bitmap, int maxWidth, int maxHeight) { - if (bitmap != null) { - final int width = bitmap.getWidth(); - final int height = bitmap.getHeight(); - if (width > maxWidth || height > maxHeight) { - float scale = Math.min((float) maxWidth / width, (float) maxHeight / height); - int newWidth = Math.round(scale * width); - int newHeight = Math.round(scale * height); - Bitmap.Config newConfig = bitmap.getConfig(); - if (newConfig == null) { - newConfig = Bitmap.Config.ARGB_8888; - } - Bitmap outBitmap = Bitmap.createBitmap(newWidth, newHeight, newConfig); - Canvas canvas = new Canvas(outBitmap); - Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setFilterBitmap(true); - canvas.drawBitmap(bitmap, null, - new RectF(0, 0, outBitmap.getWidth(), outBitmap.getHeight()), paint); - bitmap = outBitmap; - } - } - return bitmap; - } - - - /** * Returns whether, for the given playback state, the playback position is expected to * be changing. * @param playstate the playback state to evaluate diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java index 76c7299..1f5b216 100644 --- a/media/java/android/media/RemoteController.java +++ b/media/java/android/media/RemoteController.java @@ -35,6 +35,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.os.UserHandle; import android.util.DisplayMetrics; import android.util.Log; import android.view.KeyEvent; @@ -361,18 +362,10 @@ public final class RemoteController if (timeMs < 0) { throw new IllegalArgumentException("illegal negative time value"); } - if (USE_SESSIONS) { - synchronized (mInfoLock) { - if (mCurrentSession != null) { - mCurrentSession.getTransportControls().seekTo(timeMs); - } - } - } else { - final int genId; - synchronized (mGenLock) { - genId = mClientGenerationIdCurrent; + synchronized (mInfoLock) { + if (mCurrentSession != null) { + mCurrentSession.getTransportControls().seekTo(timeMs); } - mAudioManager.setRemoteControlClientPlaybackPosition(genId, timeMs); } return true; } @@ -534,34 +527,15 @@ public final class RemoteController if (!mMetadataChanged) { return; } - if (USE_SESSIONS) { - synchronized (mInfoLock) { - if (mCurrentSession != null) { - if (mEditorMetadata.containsKey( - String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) { - Rating rating = (Rating) getObject( - MediaMetadataEditor.RATING_KEY_BY_USER, null); - if (rating != null) { - mCurrentSession.getTransportControls().setRating(rating); - } - } - } - } - } else { - final int genId; - synchronized(mGenLock) { - genId = mClientGenerationIdCurrent; - } - synchronized(mInfoLock) { + synchronized (mInfoLock) { + if (mCurrentSession != null) { 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"); + if (rating != null) { + mCurrentSession.getTransportControls().setRating(rating); + } } } } @@ -820,12 +794,12 @@ public final class RemoteController final ComponentName listenerComponent = new ComponentName(mContext, mOnClientUpdateListener.getClass()); mSessionManager.addActiveSessionsListener(mSessionListener, listenerComponent, - ActivityManager.getCurrentUser()); + UserHandle.myUserId()); mSessionListener.onActiveSessionsChanged(mSessionManager .getActiveSessions(listenerComponent)); if (DEBUG) { Log.d(TAG, "Registered session listener with component " + listenerComponent - + " for user " + ActivityManager.getCurrentUser()); + + " for user " + UserHandle.myUserId()); } } @@ -836,7 +810,7 @@ public final class RemoteController mSessionManager.removeActiveSessionsListener(mSessionListener); if (DEBUG) { Log.d(TAG, "Unregistered session listener for user " - + ActivityManager.getCurrentUser()); + + UserHandle.myUserId()); } } diff --git a/media/java/android/media/SubtitleTrack.java b/media/java/android/media/SubtitleTrack.java index 06063de..9fedf63 100644 --- a/media/java/android/media/SubtitleTrack.java +++ b/media/java/android/media/SubtitleTrack.java @@ -75,6 +75,14 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList private long mNextScheduledTimeMs = -1; + protected void onData(SubtitleData data) { + long runID = data.getStartTimeUs() + 1; + onData(data.getData(), true /* eos */, runID); + setRunDiscardTimeMs( + runID, + (data.getStartTimeUs() + data.getDurationUs()) / 1000); + } + /** * Called when there is input data for the subtitle track. The * complete subtitle for a track can include multiple whole units @@ -83,7 +91,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList * indicating the last section of the run. Calls from different * runs must not be intermixed. * - * @param data + * @param data subtitle data byte buffer * @param eos true if this is the last section of the run. * @param runID mostly-unique ID for this run of data. Subtitle cues * with runID of 0 are discarded immediately after @@ -92,10 +100,8 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList * with other runID-s are discarded at the end of the * run, which defaults to the latest timestamp of * any of its cues (with this runID). - * - * TODO use ByteBuffer */ - public abstract void onData(String data, boolean eos, long runID); + public abstract void onData(byte[] data, boolean eos, long runID); /** * Called when adding the subtitle rendering widget to the view hierarchy, diff --git a/media/java/android/media/TtmlRenderer.java b/media/java/android/media/TtmlRenderer.java index 0309334..75133c9 100644 --- a/media/java/android/media/TtmlRenderer.java +++ b/media/java/android/media/TtmlRenderer.java @@ -563,28 +563,35 @@ class TtmlTrack extends SubtitleTrack implements TtmlNodeListener { } @Override - public void onData(String data, boolean eos, long runID) { - // implement intermixing restriction for TTML. - synchronized(mParser) { - if (mCurrentRunID != null && runID != mCurrentRunID) { - throw new IllegalStateException( - "Run #" + mCurrentRunID + - " in progress. Cannot process run #" + runID); - } - mCurrentRunID = runID; - mParsingData += data; - if (eos) { - try { - mParser.parse(mParsingData, mCurrentRunID); - } catch (XmlPullParserException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); + public void onData(byte[] data, boolean eos, long runID) { + try { + // TODO: handle UTF-8 conversion properly + String str = new String(data, "UTF-8"); + + // implement intermixing restriction for TTML. + synchronized(mParser) { + if (mCurrentRunID != null && runID != mCurrentRunID) { + throw new IllegalStateException( + "Run #" + mCurrentRunID + + " in progress. Cannot process run #" + runID); + } + mCurrentRunID = runID; + mParsingData += str; + if (eos) { + try { + mParser.parse(mParsingData, mCurrentRunID); + } catch (XmlPullParserException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + finishedRun(runID); + mParsingData = ""; + mCurrentRunID = null; } - finishedRun(runID); - mParsingData = ""; - mCurrentRunID = null; } + } catch (java.io.UnsupportedEncodingException e) { + Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e); } } diff --git a/media/java/android/media/session/RemoteVolumeProvider.java b/media/java/android/media/VolumeProvider.java index 606b1d7..7d93b40 100644 --- a/media/java/android/media/session/RemoteVolumeProvider.java +++ b/media/java/android/media/VolumeProvider.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.media.session; +package android.media; +import android.media.session.MediaSession; import android.os.RemoteException; import android.util.Log; @@ -24,8 +25,8 @@ import android.util.Log; * You can set a volume provider on a session by calling * {@link MediaSession#setPlaybackToRemote}. */ -public abstract class RemoteVolumeProvider { - private static final String TAG = "RemoteVolumeProvider"; +public abstract class VolumeProvider { + private static final String TAG = "VolumeProvider"; /** * The volume is fixed and can not be modified. Requests to change volume @@ -60,7 +61,7 @@ public abstract class RemoteVolumeProvider { * this provider. * @param maxVolume The maximum allowed volume. */ - public RemoteVolumeProvider(int volumeControl, int maxVolume) { + public VolumeProvider(int volumeControl, int maxVolume) { mControlType = volumeControl; mMaxVolume = maxVolume; } @@ -117,7 +118,7 @@ public abstract class RemoteVolumeProvider { /** * @hide */ - void setSession(MediaSession session) { + public void setSession(MediaSession session) { mSession = session; } }
\ No newline at end of file diff --git a/media/java/android/media/WebVttRenderer.java b/media/java/android/media/WebVttRenderer.java index 7977988..a9374d5 100644 --- a/media/java/android/media/WebVttRenderer.java +++ b/media/java/android/media/WebVttRenderer.java @@ -1001,22 +1001,28 @@ class WebVttTrack extends SubtitleTrack implements WebVttCueListener { } @Override - public void onData(String data, boolean eos, long runID) { - // implement intermixing restriction for WebVTT only for now - synchronized(mParser) { - if (mCurrentRunID != null && runID != mCurrentRunID) { - throw new IllegalStateException( - "Run #" + mCurrentRunID + - " in progress. Cannot process run #" + runID); - } - mCurrentRunID = runID; - mParser.parse(data); - if (eos) { - finishedRun(runID); - mParser.eos(); - mRegions.clear(); - mCurrentRunID = null; + public void onData(byte[] data, boolean eos, long runID) { + try { + String str = new String(data, "UTF-8"); + + // implement intermixing restriction for WebVTT only for now + synchronized(mParser) { + if (mCurrentRunID != null && runID != mCurrentRunID) { + throw new IllegalStateException( + "Run #" + mCurrentRunID + + " in progress. Cannot process run #" + runID); + } + mCurrentRunID = runID; + mParser.parse(str); + if (eos) { + finishedRun(runID); + mParser.eos(); + mRegions.clear(); + mCurrentRunID = null; + } } + } catch (java.io.UnsupportedEncodingException e) { + Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e); } } diff --git a/media/java/android/media/session/ISession.aidl b/media/java/android/media/session/ISession.aidl index 1cfc5bc..5bc0de4 100644 --- a/media/java/android/media/session/ISession.aidl +++ b/media/java/android/media/session/ISession.aidl @@ -15,6 +15,7 @@ package android.media.session; +import android.content.ComponentName; import android.media.MediaMetadata; import android.media.session.ISessionController; import android.media.session.RouteOptions; @@ -33,6 +34,7 @@ interface ISession { ISessionController getController(); void setFlags(int flags); void setActive(boolean active); + void setMediaButtonReceiver(in ComponentName mbr); void destroy(); // These commands are for setting up and communicating with routes diff --git a/media/java/android/media/session/ISessionController.aidl b/media/java/android/media/session/ISessionController.aidl index f0cd785..b4c11f6 100644 --- a/media/java/android/media/session/ISessionController.aidl +++ b/media/java/android/media/session/ISessionController.aidl @@ -20,6 +20,7 @@ import android.media.MediaMetadata; import android.media.Rating; import android.media.session.ISessionControllerCallback; import android.media.session.MediaSessionInfo; +import android.media.session.ParcelableVolumeInfo; import android.media.session.PlaybackState; import android.os.Bundle; import android.os.ResultReceiver; @@ -38,6 +39,9 @@ interface ISessionController { void showRoutePicker(); MediaSessionInfo getSessionInfo(); long getFlags(); + ParcelableVolumeInfo getVolumeAttributes(); + void adjustVolumeBy(int delta, int flags); + void setVolumeTo(int value, int flags); // These commands are for the TransportController void play(); diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java index 5ca7daa..84dad25 100644 --- a/media/java/android/media/session/MediaController.java +++ b/media/java/android/media/session/MediaController.java @@ -18,6 +18,7 @@ package android.media.session; import android.media.MediaMetadata; import android.media.Rating; +import android.media.VolumeProvider; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -70,14 +71,7 @@ public final class MediaController { * @hide */ public static MediaController fromBinder(ISessionController sessionBinder) { - MediaController controller = new MediaController(sessionBinder); - try { - controller.mSessionBinder.registerCallbackListener(controller.mCbStub); - } catch (RemoteException e) { - Log.wtf(TAG, "MediaController created with expired token", e); - controller = null; - } - return controller; + return new MediaController(sessionBinder); } /** @@ -190,6 +184,23 @@ public final class MediaController { } /** + * Get the current volume info for this session. + * + * @return The current volume info or null. + */ + public VolumeInfo getVolumeInfo() { + try { + ParcelableVolumeInfo result = mSessionBinder.getVolumeAttributes(); + return new VolumeInfo(result.volumeType, result.audioStream, result.controlType, + result.maxVolume, result.currentVolume); + + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling getVolumeInfo.", e); + } + return null; + } + + /** * Adds a callback to receive updates from the Session. Updates will be * posted on the caller's thread. * @@ -305,7 +316,7 @@ public final class MediaController { mSessionBinder.registerCallbackListener(mCbStub); mCbRegistered = true; } catch (RemoteException e) { - Log.d(TAG, "Dead object in registerCallback", e); + Log.e(TAG, "Dead object in registerCallback", e); } } } @@ -314,14 +325,23 @@ public final class MediaController { if (cb == null) { throw new IllegalArgumentException("Callback cannot be null"); } + boolean success = false; for (int i = mCallbacks.size() - 1; i >= 0; i--) { MessageHandler handler = mCallbacks.get(i); if (cb == handler.mCallback) { mCallbacks.remove(i); - return true; + success = true; } } - return false; + if (mCbRegistered && mCallbacks.size() == 0) { + try { + mSessionBinder.unregisterCallbackListener(mCbStub); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in removeCallbackLocked"); + } + mCbRegistered = false; + } + return success; } private MessageHandler getHandlerForCallbackLocked(Callback cb) { @@ -507,6 +527,85 @@ public final class MediaController { } } + /** + * Holds information about the way volume is handled for this session. + */ + public static final class VolumeInfo { + private final int mVolumeType; + private final int mAudioStream; + private final int mVolumeControl; + private final int mMaxVolume; + private final int mCurrentVolume; + + /** + * @hide + */ + public VolumeInfo(int type, int stream, int control, int max, int current) { + mVolumeType = type; + mAudioStream = stream; + mVolumeControl = control; + mMaxVolume = max; + mCurrentVolume = current; + } + + /** + * Get the type of volume handling, either local or remote. One of: + * <ul> + * <li>{@link MediaSession#VOLUME_TYPE_LOCAL}</li> + * <li>{@link MediaSession#VOLUME_TYPE_REMOTE}</li> + * </ul> + * + * @return The type of volume handling this session is using. + */ + public int getVolumeType() { + return mVolumeType; + } + + /** + * Get the stream this is currently controlling volume on. When the volume + * type is {@link MediaSession#VOLUME_TYPE_REMOTE} this value does not + * have meaning and should be ignored. + * + * @return The stream this session is playing on. + */ + public int getAudioStream() { + return mAudioStream; + } + + /** + * Get the type of volume control that can be used. One of: + * <ul> + * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li> + * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li> + * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li> + * </ul> + * + * @return The type of volume control that may be used with this + * session. + */ + public int getVolumeControl() { + return mVolumeControl; + } + + /** + * Get the maximum volume that may be set for this session. + * + * @return The maximum allowed volume where this session is playing. + */ + public int getMaxVolume() { + return mMaxVolume; + } + + /** + * Get the current volume for this session. + * + * @return The current volume where this session is playing. + */ + public int getCurrentVolume() { + return mCurrentVolume; + } + } + private final static class CallbackStub extends ISessionControllerCallback.Stub { private final WeakReference<MediaController> mController; diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 4ba1351..406b1c3 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -19,10 +19,12 @@ package android.media.session; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.PendingIntent; +import android.content.ComponentName; import android.content.Intent; import android.media.AudioManager; import android.media.MediaMetadata; import android.media.Rating; +import android.media.VolumeProvider; import android.media.session.ISessionController; import android.media.session.ISession; import android.media.session.ISessionCallback; @@ -125,18 +127,12 @@ public final class MediaSession { public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5; /** - * The session uses local playback. Used for configuring volume handling - * with the system. - * - * @hide + * The session uses local playback. */ public static final int VOLUME_TYPE_LOCAL = 1; /** - * The session uses remote playback. Used for configuring volume handling - * with the system. - * - * @hide + * The session uses remote playback. */ public static final int VOLUME_TYPE_REMOTE = 2; @@ -155,7 +151,7 @@ public final class MediaSession { = new ArrayMap<String, RouteInterface.EventListener>(); private Route mRoute; - private RemoteVolumeProvider mVolumeProvider; + private VolumeProvider mVolumeProvider; private boolean mActive = false;; @@ -232,6 +228,21 @@ public final class MediaSession { } /** + * Set a media button event receiver component to use to restart playback + * after an app has been stopped. + * + * @param mbr The receiver component to send the media button event to. + * @hide + */ + public void setMediaButtonReceiver(ComponentName mbr) { + try { + mBinder.setMediaButtonReceiver(mbr); + } catch (RemoteException e) { + Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e); + } + } + + /** * Set any flags for the session. * * @param flags The flags to set for this session. @@ -272,7 +283,7 @@ public final class MediaSession { * @param volumeProvider The provider that will handle volume changes. May * not be null. */ - public void setPlaybackToRemote(RemoteVolumeProvider volumeProvider) { + public void setPlaybackToRemote(VolumeProvider volumeProvider) { if (volumeProvider == null) { throw new IllegalArgumentException("volumeProvider may not be null!"); } @@ -524,12 +535,12 @@ public final class MediaSession { } /** - * Notify the system that the remove volume changed. + * Notify the system that the remote volume changed. * * @param provider The provider that is handling volume changes. * @hide */ - void notifyRemoteVolumeChanged(RemoteVolumeProvider provider) { + public void notifyRemoteVolumeChanged(VolumeProvider provider) { if (provider == null || provider != mVolumeProvider) { Log.w(TAG, "Received update from stale volume provider"); return; diff --git a/media/java/android/media/session/MediaSessionLegacyHelper.java b/media/java/android/media/session/MediaSessionLegacyHelper.java index 801844f..838b857 100644 --- a/media/java/android/media/session/MediaSessionLegacyHelper.java +++ b/media/java/android/media/session/MediaSessionLegacyHelper.java @@ -18,6 +18,7 @@ package android.media.session; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.media.MediaMetadata; @@ -214,7 +215,7 @@ public class MediaSessionLegacyHelper { } } - public void addMediaButtonListener(PendingIntent pi, + public void addMediaButtonListener(PendingIntent pi, ComponentName mbrComponent, Context context) { if (pi == null) { Log.w(TAG, "Pending intent was null, can't addMediaButtonListener."); @@ -238,6 +239,7 @@ public class MediaSessionLegacyHelper { holder.mMediaButtonReceiver = new MediaButtonReceiver(pi, context); holder.mSession.addCallback(holder.mMediaButtonReceiver, mHandler); + holder.mSession.setMediaButtonReceiver(mbrComponent); if (DEBUG) { Log.d(TAG, "addMediaButtonListener added " + pi); } diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java index 2e6b86e..1ff49d8 100644 --- a/media/java/android/media/session/MediaSessionManager.java +++ b/media/java/android/media/session/MediaSessionManager.java @@ -109,7 +109,6 @@ public final class MediaSessionManager { * @param notificationListener The enabled notification listener component. * May be null. * @return A list of controllers for ongoing sessions - * @hide */ public List<MediaController> getActiveSessions(ComponentName notificationListener) { return getActiveSessionsForUser(notificationListener, UserHandle.myUserId()); @@ -157,6 +156,24 @@ public final class MediaSessionManager { * @param sessionListener The listener to add. * @param notificationListener The enabled notification listener component. * May be null. + */ + public void addActiveSessionsListener(SessionListener sessionListener, + ComponentName notificationListener) { + addActiveSessionsListener(sessionListener, notificationListener, UserHandle.myUserId()); + } + + /** + * Add a listener to be notified when the list of active sessions + * changes.This requires the + * android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by + * the calling app. You may also retrieve this list if your app is an + * enabled notification listener using the + * {@link NotificationListenerService} APIs, in which case you must pass the + * {@link ComponentName} of your enabled listener. + * + * @param sessionListener The listener to add. + * @param notificationListener The enabled notification listener component. + * May be null. * @param userId The userId to listen for changes on. * @hide */ @@ -236,8 +253,6 @@ public final class MediaSessionManager { /** * Listens for changes to the list of active sessions. This can be added * using {@link #addActiveSessionsListener}. - * - * @hide */ public static abstract class SessionListener { /** diff --git a/media/java/android/media/session/ParcelableVolumeInfo.aidl b/media/java/android/media/session/ParcelableVolumeInfo.aidl new file mode 100644 index 0000000..c4250f0 --- /dev/null +++ b/media/java/android/media/session/ParcelableVolumeInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media.session; + +parcelable ParcelableVolumeInfo; diff --git a/media/java/android/media/session/ParcelableVolumeInfo.java b/media/java/android/media/session/ParcelableVolumeInfo.java new file mode 100644 index 0000000..166ccd3 --- /dev/null +++ b/media/java/android/media/session/ParcelableVolumeInfo.java @@ -0,0 +1,78 @@ +/* Copyright 2014, The Android Open Source Project + ** + ** Licensed under the Apache License, Version 2.0 (the "License"); + ** you may not use this file except in compliance with the License. + ** You may obtain a copy of the License at + ** + ** http://www.apache.org/licenses/LICENSE-2.0 + ** + ** Unless required by applicable law or agreed to in writing, software + ** distributed under the License is distributed on an "AS IS" BASIS, + ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ** See the License for the specific language governing permissions and + ** limitations under the License. + */ + +package android.media.session; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Convenience class for passing information about the audio configuration of a + * session. The public implementation is {@link MediaController.VolumeInfo}. + * + * @hide + */ +public class ParcelableVolumeInfo implements Parcelable { + public int volumeType; + public int audioStream; + public int controlType; + public int maxVolume; + public int currentVolume; + + public ParcelableVolumeInfo(int volumeType, int audioStream, int controlType, int maxVolume, + int currentVolume) { + this.volumeType = volumeType; + this.audioStream = audioStream; + this.controlType = controlType; + this.maxVolume = maxVolume; + this.currentVolume = currentVolume; + } + + public ParcelableVolumeInfo(Parcel from) { + volumeType = from.readInt(); + audioStream = from.readInt(); + controlType = from.readInt(); + maxVolume = from.readInt(); + currentVolume = from.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(volumeType); + dest.writeInt(audioStream); + dest.writeInt(controlType); + dest.writeInt(maxVolume); + dest.writeInt(currentVolume); + } + + + public static final Parcelable.Creator<ParcelableVolumeInfo> CREATOR + = new Parcelable.Creator<ParcelableVolumeInfo>() { + @Override + public ParcelableVolumeInfo createFromParcel(Parcel in) { + return new ParcelableVolumeInfo(in); + } + + @Override + public ParcelableVolumeInfo[] newArray(int size) { + return new ParcelableVolumeInfo[size]; + } + }; +} diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index 52045d3..7e9d279 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -22,7 +22,9 @@ import android.content.ContentUris; import android.net.Uri; import android.provider.BaseColumns; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * <p> @@ -144,35 +146,62 @@ public final class TvContract { /** * Builds a URI that points to all programs on a given channel. * + * @param channelId The ID of the channel to return programs for. + */ + public static final Uri buildProgramsUriForChannel(long channelId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY) + .appendPath(PATH_CHANNEL).appendPath(String.valueOf(channelId)) + .appendPath(PATH_PROGRAM).build(); + } + + /** + * Builds a URI that points to all programs on a given channel. + * * @param channelUri The URI of the channel to return programs for. */ public static final Uri buildProgramsUriForChannel(Uri channelUri) { if (!PATH_CHANNEL.equals(channelUri.getPathSegments().get(0))) { throw new IllegalArgumentException("Not a channel: " + channelUri); } - String channelId = String.valueOf(ContentUris.parseId(channelUri)); - return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY) - .appendPath(PATH_CHANNEL).appendPath(channelId).appendPath(PATH_PROGRAM).build(); + return buildProgramsUriForChannel(ContentUris.parseId(channelUri)); } /** * Builds a URI that points to programs on a specific channel whose schedules overlap with the * given time frame. * - * @param channelUri The URI of the channel to return programs for. + * @param channelId The ID of the channel to return programs for. * @param startTime The start time used to filter programs. The returned programs should have * {@link Programs#COLUMN_END_TIME_UTC_MILLIS} that is greater than this time. * @param endTime The end time used to filter programs. The returned programs should have * {@link Programs#COLUMN_START_TIME_UTC_MILLIS} that is less than this time. */ - public static final Uri buildProgramsUriForChannel(Uri channelUri, long startTime, + public static final Uri buildProgramsUriForChannel(long channelId, long startTime, long endTime) { - Uri uri = buildProgramsUriForChannel(channelUri); + Uri uri = buildProgramsUriForChannel(channelId); return uri.buildUpon().appendQueryParameter(PARAM_START_TIME, String.valueOf(startTime)) .appendQueryParameter(PARAM_END_TIME, String.valueOf(endTime)).build(); } /** + * Builds a URI that points to programs on a specific channel whose schedules overlap with the + * given time frame. + * + * @param channelUri The URI of the channel to return programs for. + * @param startTime The start time used to filter programs. The returned programs should have + * {@link Programs#COLUMN_END_TIME_UTC_MILLIS} that is greater than this time. + * @param endTime The end time used to filter programs. The returned programs should have + * {@link Programs#COLUMN_START_TIME_UTC_MILLIS} that is less than this time. + */ + public static final Uri buildProgramsUriForChannel(Uri channelUri, long startTime, + long endTime) { + if (!PATH_CHANNEL.equals(channelUri.getPathSegments().get(0))) { + throw new IllegalArgumentException("Not a channel: " + channelUri); + } + return buildProgramsUriForChannel(ContentUris.parseId(channelUri), startTime, endTime); + } + + /** * Builds a URI that points to a specific program the user watched. * * @param watchedProgramId The ID of the watched program to point to. @@ -272,6 +301,15 @@ public final class TvContract { /** A generic channel type. */ public static final int TYPE_OTHER = 0x0; + /** The channel type for NTSC. */ + public static final int TYPE_NTSC = 0x1; + + /** The channel type for PAL. */ + public static final int TYPE_PAL = 0x2; + + /** The channel type for SECAM. */ + public static final int TYPE_SECAM = 0x3; + /** The special channel type used for pass-through inputs such as HDMI. */ public static final int TYPE_PASSTHROUGH = 0x00010000; @@ -344,6 +382,81 @@ public final class TvContract { /** The service type for radio channels that have audio only. */ public static final int SERVICE_TYPE_AUDIO = 0x2; + /** The video format for 240p. */ + public static final String VIDEO_FORMAT_240P = "VIDEO_FORMAT_240P"; + + /** The video format for 360p. */ + public static final String VIDEO_FORMAT_360P = "VIDEO_FORMAT_360P"; + + /** The video format for 480i. */ + public static final String VIDEO_FORMAT_480I = "VIDEO_FORMAT_480I"; + + /** The video format for 480p. */ + public static final String VIDEO_FORMAT_480P = "VIDEO_FORMAT_480P"; + + /** The video format for 576i. */ + public static final String VIDEO_FORMAT_576I = "VIDEO_FORMAT_576I"; + + /** The video format for 576p. */ + public static final String VIDEO_FORMAT_576P = "VIDEO_FORMAT_576P"; + + /** The video format for 720p. */ + public static final String VIDEO_FORMAT_720P = "VIDEO_FORMAT_720P"; + + /** The video format for 1080i. */ + public static final String VIDEO_FORMAT_1080I = "VIDEO_FORMAT_1080I"; + + /** The video format for 1080p. */ + public static final String VIDEO_FORMAT_1080P = "VIDEO_FORMAT_1080P"; + + /** The video format for 2160p. */ + public static final String VIDEO_FORMAT_2160P = "VIDEO_FORMAT_2160P"; + + /** The video format for 4320p. */ + public static final String VIDEO_FORMAT_4320P = "VIDEO_FORMAT_4320P"; + + /** The video resolution for standard-definition. */ + public static final String VIDEO_RESOLUTION_SD = "VIDEO_RESOLUTION_SD"; + + /** The video resolution for enhanced-definition. */ + public static final String VIDEO_RESOLUTION_ED = "VIDEO_RESOLUTION_ED"; + + /** The video resolution for high-definition. */ + public static final String VIDEO_RESOLUTION_HD = "VIDEO_RESOLUTION_HD"; + + /** The video resolution for full high-definition. */ + public static final String VIDEO_RESOLUTION_FHD = "VIDEO_RESOLUTION_FHD"; + + /** The video resolution for ultra high-definition. */ + public static final String VIDEO_RESOLUTION_UHD = "VIDEO_RESOLUTION_UHD"; + + private static final Map<String, String> VIDEO_FORMAT_TO_RESOLUTION_MAP = + new HashMap<String, String>(); + + static { + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_480I, VIDEO_RESOLUTION_SD); + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_480P, VIDEO_RESOLUTION_ED); + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_576I, VIDEO_RESOLUTION_SD); + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_576P, VIDEO_RESOLUTION_ED); + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_720P, VIDEO_RESOLUTION_HD); + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_1080I, VIDEO_RESOLUTION_HD); + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_1080P, VIDEO_RESOLUTION_FHD); + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_2160P, VIDEO_RESOLUTION_UHD); + VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_4320P, VIDEO_RESOLUTION_UHD); + } + + /** + * Returns the video resolution (definition) for a given video format. + * + * @param videoFormat The video format defined in {@link Channels}. + * @return the corresponding video resolution string. {@code null} if the resolution string + * is not defined for the given video format. + * @see #COLUMN_VIDEO_FORMAT + */ + public static final String getVideoResolution(String videoFormat) { + return VIDEO_FORMAT_TO_RESOLUTION_MAP.get(videoFormat); + } + /** * The name of the {@link TvInputService} subclass that provides this TV channel. This * should be a fully qualified class name (such as, "com.example.project.TvInputService"). @@ -477,6 +590,24 @@ public final class TvContract { public static final String COLUMN_DESCRIPTION = "description"; /** + * The typical video format for programs from this TV channel. + * <p> + * This is primarily used to filter out channels based on video format by applications. The + * value should match one of the followings: {@link #VIDEO_FORMAT_240P}, + * {@link #VIDEO_FORMAT_360P}, {@link #VIDEO_FORMAT_480I}, {@link #VIDEO_FORMAT_480P}, + * {@link #VIDEO_FORMAT_576I}, {@link #VIDEO_FORMAT_576P}, {@link #VIDEO_FORMAT_720P}, + * {@link #VIDEO_FORMAT_1080I}, {@link #VIDEO_FORMAT_1080P}, {@link #VIDEO_FORMAT_2160P}, + * {@link #VIDEO_FORMAT_4320P}. Note that the actual video resolution of each program from a + * given channel can vary thus one should use {@link Programs#COLUMN_VIDEO_WIDTH} and + * {@link Programs#COLUMN_VIDEO_HEIGHT} to get more accurate video resolution. + * </p><p> + * Type: TEXT + * </p><p> + * @see #getVideoResolution + */ + public static final String COLUMN_VIDEO_FORMAT = "video_format"; + + /** * The flag indicating whether this TV channel is browsable or not. * <p> * A value of 1 indicates the channel is included in the channel list that applications use @@ -683,6 +814,32 @@ public final class TvContract { public static final String COLUMN_LONG_DESCRIPTION = "long_description"; /** + * The width of the video for this TV program, in the unit of pixels. + * <p> + * Together with {@link #COLUMN_VIDEO_HEIGHT} this is used to determine the video resolution + * of the current TV program. Can be empty if it is not known initially or the program does + * not convey any video such as the programs from type {@link Channels#SERVICE_TYPE_AUDIO} + * channels. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_VIDEO_WIDTH = "video_width"; + + /** + * The height of the video for this TV program, in the unit of pixels. + * <p> + * Together with {@link #COLUMN_VIDEO_WIDTH} this is used to determine the video resolution + * of the current TV program. Can be empty if it is not known initially or the program does + * not convey any video such as the programs from type {@link Channels#SERVICE_TYPE_AUDIO} + * channels. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_VIDEO_HEIGHT = "video_height"; + + /** * The comma-separated audio languages of this TV program. * <p> * This is used to describe available audio languages included in the program. Use @@ -742,37 +899,37 @@ public final class TvContract { /** Canonical genres for TV programs. */ public static final class Genres { /** The genre for Family/Kids. */ - public static final String FAMILY_KIDS = "Family/Kids"; + public static final String FAMILY_KIDS = "FAMILY_KIDS"; /** The genre for Sports. */ - public static final String SPORTS = "Sports"; + public static final String SPORTS = "SPORTS"; /** The genre for Shopping. */ - public static final String SHOPPING = "Shopping"; + public static final String SHOPPING = "SHOPPING"; /** The genre for Movies. */ - public static final String MOVIES = "Movies"; + public static final String MOVIES = "MOVIES"; /** The genre for Comedy. */ - public static final String COMEDY = "Comedy"; + public static final String COMEDY = "COMEDY"; /** The genre for Travel. */ - public static final String TRAVEL = "Travel"; + public static final String TRAVEL = "TRAVEL"; /** The genre for Drama. */ - public static final String DRAMA = "Drama"; + public static final String DRAMA = "DRAMA"; /** The genre for Education. */ - public static final String EDUCATION = "Education"; + public static final String EDUCATION = "EDUCATION"; /** The genre for Animal/Wildlife. */ - public static final String ANIMAL_WILDLIFE = "Animal/Wildlife"; + public static final String ANIMAL_WILDLIFE = "ANIMAL_WILDLIFE"; /** The genre for News. */ - public static final String NEWS = "News"; + public static final String NEWS = "NEWS"; /** The genre for Gaming. */ - public static final String GAMING = "Gaming"; + public static final String GAMING = "GAMING"; private Genres() {} @@ -812,7 +969,7 @@ public final class TvContract { * * @hide */ - public static final class WatchedPrograms implements BaseColumns { + public static final class WatchedPrograms implements BaseTvColumns { /** The content:// style URI for this table. */ public static final Uri CONTENT_URI = diff --git a/media/java/android/media/tv/TvInputHardwareInfo.java b/media/java/android/media/tv/TvInputHardwareInfo.java index 4beb960..e5f9889 100644 --- a/media/java/android/media/tv/TvInputHardwareInfo.java +++ b/media/java/android/media/tv/TvInputHardwareInfo.java @@ -16,6 +16,7 @@ package android.media.tv; +import android.media.AudioManager; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; @@ -56,14 +57,11 @@ public final class TvInputHardwareInfo implements Parcelable { private int mDeviceId; private int mType; - // TODO: Add audio port & audio address for audio service. - // TODO: Add HDMI handle for HDMI service. + private int mAudioType; + private String mAudioAddress; + private int mHdmiPortId; - public TvInputHardwareInfo() { } - - public TvInputHardwareInfo(int deviceId, int type) { - mDeviceId = deviceId; - mType = type; + private TvInputHardwareInfo() { } public int getDeviceId() { @@ -74,6 +72,21 @@ public final class TvInputHardwareInfo implements Parcelable { return mType; } + public int getAudioType() { + return mAudioType; + } + + public String getAudioAddress() { + return mAudioAddress; + } + + public int getHdmiPortId() { + if (mType != TV_INPUT_TYPE_HDMI) { + throw new IllegalStateException(); + } + return mHdmiPortId; + } + // Parcelable @Override public int describeContents() { @@ -84,10 +97,78 @@ public final class TvInputHardwareInfo implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mDeviceId); dest.writeInt(mType); + dest.writeInt(mAudioType); + dest.writeString(mAudioAddress); + if (mType == TV_INPUT_TYPE_HDMI) { + dest.writeInt(mHdmiPortId); + } } public void readFromParcel(Parcel source) { mDeviceId = source.readInt(); mType = source.readInt(); + mAudioType = source.readInt(); + mAudioAddress = source.readString(); + if (mType == TV_INPUT_TYPE_HDMI) { + mHdmiPortId = source.readInt(); + } + } + + public static final class Builder { + private Integer mDeviceId = null; + private Integer mType = null; + private int mAudioType = AudioManager.DEVICE_NONE; + private String mAudioAddress = ""; + private Integer mHdmiPortId = null; + + public Builder() { + } + + public Builder deviceId(int deviceId) { + mDeviceId = deviceId; + return this; + } + + public Builder type(int type) { + mType = type; + return this; + } + + public Builder audioType(int audioType) { + mAudioType = audioType; + return this; + } + + public Builder audioAddress(String audioAddress) { + mAudioAddress = audioAddress; + return this; + } + + public Builder hdmiPortId(int hdmiPortId) { + mHdmiPortId = hdmiPortId; + return this; + } + + public TvInputHardwareInfo build() { + if (mDeviceId == null || mType == null) { + throw new UnsupportedOperationException(); + } + if ((mType == TV_INPUT_TYPE_HDMI && mHdmiPortId == null) || + (mType != TV_INPUT_TYPE_HDMI && mHdmiPortId != null)) { + throw new UnsupportedOperationException(); + } + + TvInputHardwareInfo info = new TvInputHardwareInfo(); + info.mDeviceId = mDeviceId; + info.mType = mType; + info.mAudioType = mAudioType; + if (info.mAudioType != AudioManager.DEVICE_NONE) { + info.mAudioAddress = mAudioAddress; + } + if (mHdmiPortId != null) { + info.mHdmiPortId = mHdmiPortId; + } + return info; + } } } diff --git a/media/java/android/media/tv/TvInputInfo.java b/media/java/android/media/tv/TvInputInfo.java index 868c5bf..7b8f2ec 100644 --- a/media/java/android/media/tv/TvInputInfo.java +++ b/media/java/android/media/tv/TvInputInfo.java @@ -26,6 +26,7 @@ import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -223,6 +224,18 @@ public final class TvInputInfo implements Parcelable { return mService.loadLabel(pm); } + /** + * Loads the user-displayed icon for this TV input service. + * + * @param pm Supplies a PackageManager used to load the TV input's resources. + * @return a Drawable containing the TV input's icon. If the TV input does not have + * an icon, application icon is returned. If it's unavailable too, system default is + * returned. + */ + public Drawable loadIcon(PackageManager pm) { + return mService.serviceInfo.loadIcon(pm); + } + @Override public int describeContents() { return 0; diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index edfdd60..daa7009 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -533,12 +533,11 @@ public final class TvInputManager { /** * Releases this session. - * - * @throws IllegalStateException if the session has been already released. */ public void release() { if (mToken == null) { - throw new IllegalStateException("the session has been already released"); + Log.w(TAG, "The session has been already released"); + return; } try { mService.releaseSession(mToken, mUserId); @@ -553,12 +552,12 @@ public final class TvInputManager { * Sets the {@link android.view.Surface} for this session. * * @param surface A {@link android.view.Surface} used to render video. - * @throws IllegalStateException if the session has been already released. * @hide */ public void setSurface(Surface surface) { if (mToken == null) { - throw new IllegalStateException("the session has been already released"); + Log.w(TAG, "The session has been already released"); + return; } // surface can be null. try { @@ -573,11 +572,11 @@ public final class TvInputManager { * * @param volume A volume value between 0.0f to 1.0f. * @throws IllegalArgumentException if the volume value is out of range. - * @throws IllegalStateException if the session has been already released. */ public void setStreamVolume(float volume) { if (mToken == null) { - throw new IllegalStateException("the session has been already released"); + Log.w(TAG, "The session has been already released"); + return; } try { if (volume < 0.0f || volume > 1.0f) { @@ -594,14 +593,14 @@ public final class TvInputManager { * * @param channelUri The URI of a channel. * @throws IllegalArgumentException if the argument is {@code null}. - * @throws IllegalStateException if the session has been already released. */ public void tune(Uri channelUri) { if (channelUri == null) { throw new IllegalArgumentException("channelUri cannot be null"); } if (mToken == null) { - throw new IllegalStateException("the session has been already released"); + Log.w(TAG, "The session has been already released"); + return; } try { mService.tune(mToken, channelUri, mUserId); @@ -620,8 +619,7 @@ public final class TvInputManager { * @param view A view playing TV. * @param frame A position of the overlay view. * @throws IllegalArgumentException if any of the arguments is {@code null}. - * @throws IllegalStateException if {@code view} is not attached to a window or - * if the session has been already released. + * @throws IllegalStateException if {@code view} is not attached to a window. */ void createOverlayView(View view, Rect frame) { if (view == null) { @@ -634,7 +632,8 @@ public final class TvInputManager { throw new IllegalStateException("view must be attached to a window"); } if (mToken == null) { - throw new IllegalStateException("the session has been already released"); + Log.w(TAG, "The session has been already released"); + return; } try { mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId); @@ -648,14 +647,14 @@ public final class TvInputManager { * * @param frame A new position of the overlay view. * @throws IllegalArgumentException if the arguments is {@code null}. - * @throws IllegalStateException if the session has been already released. */ void relayoutOverlayView(Rect frame) { if (frame == null) { throw new IllegalArgumentException("frame cannot be null"); } if (mToken == null) { - throw new IllegalStateException("the session has been already released"); + Log.w(TAG, "The session has been already released"); + return; } try { mService.relayoutOverlayView(mToken, frame, mUserId); @@ -666,12 +665,11 @@ public final class TvInputManager { /** * Removes the current overlay view. - * - * @throws IllegalStateException if the session has been already released. */ void removeOverlayView() { if (mToken == null) { - throw new IllegalStateException("the session has been already released"); + Log.w(TAG, "The session has been already released"); + return; } try { mService.removeOverlayView(mToken, mUserId); |