diff options
Diffstat (limited to 'media/java/android')
93 files changed, 20218 insertions, 1928 deletions
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 3b9a5de00707..44a2ff9e8bf7 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -19,12 +19,14 @@ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.media.AudioAttributesProto; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; +import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -177,7 +179,8 @@ public final class AudioAttributes implements Parcelable { /** * IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES - * if applicable. + * if applicable, as well as audioattributes.proto. + * Also consider adding them to <aaudio/AAudio.h> for the NDK. */ /** @@ -200,6 +203,22 @@ public final class AudioAttributes implements Parcelable { * @see #SUPPRESSIBLE_USAGES */ public final static int SUPPRESSIBLE_NEVER = 3; + /** + * @hide + * Denotes a usage for alarms, + * will be muted when the Zen mode doesn't allow alarms + * @see #SUPPRESSIBLE_USAGES + */ + public final static int SUPPRESSIBLE_ALARM = 4; + /** + * @hide + * Denotes a usage for all other sounds not caught in SUPPRESSIBLE_NOTIFICATION, + * SUPPRESSIBLE_CALL,SUPPRESSIBLE_NEVER or SUPPRESSIBLE_ALARM. + * This includes media, system, game, navigation, the assistant, and more. + * These will be muted when the Zen mode doesn't allow media/system/other. + * @see #SUPPRESSIBLE_USAGES + */ + public final static int SUPPRESSIBLE_MEDIA_SYSTEM_OTHER = 5; /** * @hide @@ -219,6 +238,14 @@ public final class AudioAttributes implements Parcelable { SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_EVENT, SUPPRESSIBLE_NOTIFICATION); SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_ACCESSIBILITY, SUPPRESSIBLE_NEVER); SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION, SUPPRESSIBLE_NEVER); + SUPPRESSIBLE_USAGES.put(USAGE_ALARM, SUPPRESSIBLE_ALARM); + SUPPRESSIBLE_USAGES.put(USAGE_MEDIA, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_GAME, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); } /** @@ -715,7 +742,7 @@ public final class AudioAttributes implements Parcelable { /** * @hide * Same as {@link #setCapturePreset(int)} but authorizes the use of HOTWORD, - * REMOTE_SUBMIX and RADIO_TUNER. + * REMOTE_SUBMIX, RADIO_TUNER, VOICE_DOWNLINK, VOICE_UPLINK and VOICE_CALL. * @param preset * @return the same Builder instance. */ @@ -723,7 +750,10 @@ public final class AudioAttributes implements Parcelable { public Builder setInternalCapturePreset(int preset) { if ((preset == MediaRecorder.AudioSource.HOTWORD) || (preset == MediaRecorder.AudioSource.REMOTE_SUBMIX) - || (preset == MediaRecorder.AudioSource.RADIO_TUNER)) { + || (preset == MediaRecorder.AudioSource.RADIO_TUNER) + || (preset == MediaRecorder.AudioSource.VOICE_DOWNLINK) + || (preset == MediaRecorder.AudioSource.VOICE_UPLINK) + || (preset == MediaRecorder.AudioSource.VOICE_CALL)) { mSource = preset; } else { setCapturePreset(preset); @@ -850,6 +880,25 @@ public final class AudioAttributes implements Parcelable { } /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + proto.write(AudioAttributesProto.USAGE, mUsage); + proto.write(AudioAttributesProto.CONTENT_TYPE, mContentType); + proto.write(AudioAttributesProto.FLAGS, mFlags); + // mFormattedTags is never null due to assignment in Builder or unmarshalling. + for (String t : mFormattedTags.split(";")) { + t = t.trim(); + if (t != "") { + proto.write(AudioAttributesProto.TAGS, t); + } + } + // TODO: is the data in mBundle useful for debugging? + + proto.end(token); + } + + /** @hide */ public String usageToString() { return usageToString(mUsage); } diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java index 1b89c96602d3..3d879f5a4660 100644 --- a/media/java/android/media/AudioDeviceInfo.java +++ b/media/java/android/media/AudioDeviceInfo.java @@ -16,9 +16,13 @@ package android.media; +import android.annotation.IntDef; import android.annotation.NonNull; import android.util.SparseIntArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; import java.util.TreeSet; /** @@ -120,6 +124,72 @@ public final class AudioDeviceInfo { */ public static final int TYPE_USB_HEADSET = 22; + /** @hide */ + @IntDef(flag = false, prefix = "TYPE", value = { + TYPE_BUILTIN_EARPIECE, + TYPE_BUILTIN_SPEAKER, + TYPE_WIRED_HEADSET, + TYPE_WIRED_HEADPHONES, + TYPE_BLUETOOTH_SCO, + TYPE_BLUETOOTH_A2DP, + TYPE_HDMI, + TYPE_DOCK, + TYPE_USB_ACCESSORY, + TYPE_USB_DEVICE, + TYPE_USB_HEADSET, + TYPE_TELEPHONY, + TYPE_LINE_ANALOG, + TYPE_HDMI_ARC, + TYPE_LINE_DIGITAL, + TYPE_FM, + TYPE_AUX_LINE, + TYPE_IP, + TYPE_BUS } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface AudioDeviceTypeOut {} + + /** @hide */ + /*package*/ static boolean isValidAudioDeviceTypeOut(int type) { + switch (type) { + case TYPE_BUILTIN_EARPIECE: + case TYPE_BUILTIN_SPEAKER: + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + case TYPE_BLUETOOTH_SCO: + case TYPE_BLUETOOTH_A2DP: + case TYPE_HDMI: + case TYPE_DOCK: + case TYPE_USB_ACCESSORY: + case TYPE_USB_DEVICE: + case TYPE_USB_HEADSET: + case TYPE_TELEPHONY: + case TYPE_LINE_ANALOG: + case TYPE_HDMI_ARC: + case TYPE_LINE_DIGITAL: + case TYPE_FM: + case TYPE_AUX_LINE: + case TYPE_IP: + case TYPE_BUS: + return true; + default: + return false; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AudioDeviceInfo that = (AudioDeviceInfo) o; + return Objects.equals(getPort(), that.getPort()); + } + + @Override + public int hashCode() { + return Objects.hash(getPort()); + } + private final AudioDevicePort mPort; AudioDeviceInfo(AudioDevicePort port) { @@ -127,6 +197,14 @@ public final class AudioDeviceInfo { } /** + * @hide + * @return The underlying {@link AudioDevicePort} instance. + */ + public AudioDevicePort getPort() { + return mPort; + } + + /** * @return The internal device ID. */ public int getId() { diff --git a/media/java/android/media/AudioFocusInfo.java b/media/java/android/media/AudioFocusInfo.java index 6d9c5e2ad5fc..5467a69ea0bb 100644 --- a/media/java/android/media/AudioFocusInfo.java +++ b/media/java/android/media/AudioFocusInfo.java @@ -38,6 +38,10 @@ public final class AudioFocusInfo implements Parcelable { private int mLossReceived; private int mFlags; + // generation count for the validity of a request/response async exchange between + // external focus policy and MediaFocusControl + private long mGenCount = -1; + /** * Class constructor @@ -61,6 +65,16 @@ public final class AudioFocusInfo implements Parcelable { mSdkTarget = sdk; } + /** @hide */ + public void setGen(long g) { + mGenCount = g; + } + + /** @hide */ + public long getGen() { + return mGenCount; + } + /** * The audio attributes for the audio focus request. @@ -128,15 +142,14 @@ public final class AudioFocusInfo implements Parcelable { dest.writeInt(mLossReceived); dest.writeInt(mFlags); dest.writeInt(mSdkTarget); + dest.writeLong(mGenCount); } - @SystemApi @Override public int hashCode() { return Objects.hash(mAttributes, mClientUid, mClientId, mPackageName, mGainRequest, mFlags); } - @SystemApi @Override public boolean equals(Object obj) { if (this == obj) @@ -170,6 +183,8 @@ public final class AudioFocusInfo implements Parcelable { if (mSdkTarget != other.mSdkTarget) { return false; } + // mGenCount is not used to verify equality between two focus holds as multiple requests + // (hence of different generations) could correspond to the same hold return true; } @@ -177,7 +192,7 @@ public final class AudioFocusInfo implements Parcelable { = new Parcelable.Creator<AudioFocusInfo>() { public AudioFocusInfo createFromParcel(Parcel in) { - return new AudioFocusInfo( + final AudioFocusInfo afi = new AudioFocusInfo( AudioAttributes.CREATOR.createFromParcel(in), //AudioAttributes aa in.readInt(), // int clientUid in.readString(), //String clientId @@ -187,6 +202,8 @@ public final class AudioFocusInfo implements Parcelable { in.readInt(), //int flags in.readInt() //int sdkTarget ); + afi.setGen(in.readLong()); + return afi; } public AudioFocusInfo[] newArray(int size) { diff --git a/media/java/android/media/AudioFocusRequest.java b/media/java/android/media/AudioFocusRequest.java index 9841815a52d1..7104dad4dc4c 100644 --- a/media/java/android/media/AudioFocusRequest.java +++ b/media/java/android/media/AudioFocusRequest.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.media.AudioManager.OnAudioFocusChangeListener; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -163,12 +164,12 @@ import android.os.Looper; * // requesting audio focus * int res = mAudioManager.requestAudioFocus(mFocusRequest); * synchronized (mFocusLock) { - * if (res == AUDIOFOCUS_REQUEST_FAILED) { + * if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { * mPlaybackDelayed = false; - * } else if (res == AUDIOFOCUS_REQUEST_GRANTED) { + * } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { * mPlaybackDelayed = false; * playbackNow(); - * } else if (res == AUDIOFOCUS_REQUEST_DELAYED) { + * } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) { * mPlaybackDelayed = true; * } * } @@ -220,6 +221,9 @@ public final class AudioFocusRequest { private final static AudioAttributes FOCUS_DEFAULT_ATTR = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA).build(); + /** @hide */ + public static final String KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING = "a11y_force_ducking"; + private final OnAudioFocusChangeListener mFocusListener; // may be null private final Handler mListenerHandler; // may be null private final AudioAttributes mAttr; // never null @@ -349,6 +353,7 @@ public final class AudioFocusRequest { private boolean mPausesOnDuck = false; private boolean mDelayedFocus = false; private boolean mFocusLocked = false; + private boolean mA11yForceDucking = false; /** * Constructs a new {@code Builder}, and specifies how audio focus @@ -526,6 +531,21 @@ public final class AudioFocusRequest { } /** + * Marks this focus request as forcing ducking, regardless of the conditions in which + * the system would or would not enforce ducking. + * Forcing ducking will only be honored when requesting AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + * with an {@link AudioAttributes} usage of + * {@link AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY}, coming from an accessibility + * service, and will be ignored otherwise. + * @param forceDucking {@code true} to force ducking + * @return this {@code Builder} instance + */ + public @NonNull Builder setForceDucking(boolean forceDucking) { + mA11yForceDucking = forceDucking; + return this; + } + + /** * Builds a new {@code AudioFocusRequest} instance combining all the information gathered * by this {@code Builder}'s configuration methods. * @return the {@code AudioFocusRequest} instance qualified by all the properties set @@ -538,6 +558,17 @@ public final class AudioFocusRequest { throw new IllegalStateException( "Can't use delayed focus or pause on duck without a listener"); } + if (mA11yForceDucking) { + final Bundle extraInfo; + if (mAttr.getBundle() == null) { + extraInfo = new Bundle(); + } else { + extraInfo = mAttr.getBundle(); + } + // checking of usage and focus request is done server side + extraInfo.putBoolean(KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING, true); + mAttr = new AudioAttributes.Builder(mAttr).addBundle(extraInfo).build(); + } final int flags = 0 | (mDelayedFocus ? AudioManager.AUDIOFOCUS_FLAG_DELAY_OK : 0) | (mPausesOnDuck ? AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS : 0) diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index 93fc3da54550..f98480b28001 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -238,22 +238,15 @@ public final class AudioFormat implements Parcelable { public static final int ENCODING_DTS = 7; /** Audio data format: DTS HD compressed */ public static final int ENCODING_DTS_HD = 8; - /** Audio data format: MP3 compressed - * @hide - * */ + /** Audio data format: MP3 compressed */ public static final int ENCODING_MP3 = 9; - /** Audio data format: AAC LC compressed - * @hide - * */ + /** Audio data format: AAC LC compressed */ public static final int ENCODING_AAC_LC = 10; - /** Audio data format: AAC HE V1 compressed - * @hide - * */ + /** Audio data format: AAC HE V1 compressed */ public static final int ENCODING_AAC_HE_V1 = 11; - /** Audio data format: AAC HE V2 compressed - * @hide - * */ + /** Audio data format: AAC HE V2 compressed */ public static final int ENCODING_AAC_HE_V2 = 12; + /** Audio data format: compressed audio wrapped in PCM for HDMI * or S/PDIF passthrough. * IEC61937 uses a stereo stream of 16-bit samples as the wrapper. @@ -266,6 +259,18 @@ public final class AudioFormat implements Parcelable { /** Audio data format: DOLBY TRUEHD compressed **/ public static final int ENCODING_DOLBY_TRUEHD = 14; + /** Audio data format: AAC ELD compressed */ + public static final int ENCODING_AAC_ELD = 15; + /** Audio data format: AAC xHE compressed */ + public static final int ENCODING_AAC_XHE = 16; + /** Audio data format: AC-4 sync frame transport format */ + public static final int ENCODING_AC4 = 17; + /** Audio data format: E-AC-3-JOC compressed + * E-AC-3-JOC streams can be decoded by downstream devices supporting {@link #ENCODING_E_AC3}. + * Use {@link #ENCODING_E_AC3} as the AudioTrack encoding when the downstream device + * supports {@link #ENCODING_E_AC3} but not {@link #ENCODING_E_AC3_JOC}. + **/ + public static final int ENCODING_E_AC3_JOC = 18; /** @hide */ public static String toLogFriendlyEncoding(int enc) { @@ -298,6 +303,12 @@ public final class AudioFormat implements Parcelable { return "ENCODING_IEC61937"; case ENCODING_DOLBY_TRUEHD: return "ENCODING_DOLBY_TRUEHD"; + case ENCODING_AAC_ELD: + return "ENCODING_AAC_ELD"; + case ENCODING_AAC_XHE: + return "ENCODING_AAC_XHE"; + case ENCODING_AC4: + return "ENCODING_AC4"; default : return "invalid encoding " + enc; } @@ -507,6 +518,7 @@ public final class AudioFormat implements Parcelable { case ENCODING_PCM_FLOAT: case ENCODING_AC3: case ENCODING_E_AC3: + case ENCODING_E_AC3_JOC: case ENCODING_DTS: case ENCODING_DTS_HD: case ENCODING_MP3: @@ -514,6 +526,9 @@ public final class AudioFormat implements Parcelable { case ENCODING_AAC_HE_V1: case ENCODING_AAC_HE_V2: case ENCODING_IEC61937: + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: + case ENCODING_AC4: return true; default: return false; @@ -529,9 +544,17 @@ public final class AudioFormat implements Parcelable { case ENCODING_PCM_FLOAT: case ENCODING_AC3: case ENCODING_E_AC3: + case ENCODING_E_AC3_JOC: case ENCODING_DTS: case ENCODING_DTS_HD: case ENCODING_IEC61937: + case ENCODING_MP3: + case ENCODING_AAC_LC: + case ENCODING_AAC_HE_V1: + case ENCODING_AAC_HE_V2: + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: + case ENCODING_AC4: return true; default: return false; @@ -549,6 +572,7 @@ public final class AudioFormat implements Parcelable { return true; case ENCODING_AC3: case ENCODING_E_AC3: + case ENCODING_E_AC3_JOC: case ENCODING_DTS: case ENCODING_DTS_HD: case ENCODING_MP3: @@ -556,6 +580,9 @@ public final class AudioFormat implements Parcelable { case ENCODING_AAC_HE_V1: case ENCODING_AAC_HE_V2: case ENCODING_IEC61937: // wrapped in PCM but compressed + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: + case ENCODING_AC4: return false; case ENCODING_INVALID: default: @@ -575,12 +602,16 @@ public final class AudioFormat implements Parcelable { return true; case ENCODING_AC3: case ENCODING_E_AC3: + case ENCODING_E_AC3_JOC: case ENCODING_DTS: case ENCODING_DTS_HD: case ENCODING_MP3: case ENCODING_AAC_LC: case ENCODING_AAC_HE_V1: case ENCODING_AAC_HE_V2: + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: + case ENCODING_AC4: return false; case ENCODING_INVALID: default: @@ -794,14 +825,7 @@ public final class AudioFormat implements Parcelable { /** * 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}. - * {@link AudioFormat#ENCODING_DTS}, - * {@link AudioFormat#ENCODING_DTS_HD}. + * @param encoding the specified encoding or default. * @return the same Builder instance. * @throws java.lang.IllegalArgumentException */ @@ -815,9 +839,17 @@ public final class AudioFormat implements Parcelable { case ENCODING_PCM_FLOAT: case ENCODING_AC3: case ENCODING_E_AC3: + case ENCODING_E_AC3_JOC: case ENCODING_DTS: case ENCODING_DTS_HD: case ENCODING_IEC61937: + case ENCODING_MP3: + case ENCODING_AAC_LC: + case ENCODING_AAC_HE_V1: + case ENCODING_AAC_HE_V2: + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: + case ENCODING_AC4: mEncoding = encoding; break; case ENCODING_INVALID: @@ -1016,17 +1048,24 @@ public final class AudioFormat implements Parcelable { } /** @hide */ - @IntDef({ + @IntDef(flag = false, prefix = "ENCODING", value = { ENCODING_DEFAULT, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3, + ENCODING_E_AC3_JOC, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_IEC61937 - }) + ENCODING_IEC61937, + ENCODING_AAC_HE_V1, + ENCODING_AAC_HE_V2, + ENCODING_AAC_LC, + ENCODING_AAC_ELD, + ENCODING_AAC_XHE, + ENCODING_AC4 } + ) @Retention(RetentionPolicy.SOURCE) public @interface Encoding {} diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 339c7678bef8..9ff964b3f0d4 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -16,6 +16,8 @@ package android.media; +import android.annotation.CallbackExecutor; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -31,6 +33,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.media.audiopolicy.AudioPolicy; +import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionLegacyHelper; @@ -43,20 +46,28 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; -import android.os.SystemClock; import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; -import android.util.Pair; +import android.util.Slog; import android.view.KeyEvent; +import com.android.internal.annotations.GuardedBy; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; + /** * AudioManager provides access to volume and ringer mode control. @@ -398,6 +409,18 @@ public class AudioManager { public static final int ADJUST_TOGGLE_MUTE = 101; /** @hide */ + @IntDef(flag = false, prefix = "ADJUST", value = { + ADJUST_RAISE, + ADJUST_LOWER, + ADJUST_SAME, + ADJUST_MUTE, + ADJUST_UNMUTE, + ADJUST_TOGGLE_MUTE } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface VolumeAdjustment {} + + /** @hide */ public static final String adjustToString(int adj) { switch (adj) { case ADJUST_RAISE: return "ADJUST_RAISE"; @@ -912,13 +935,28 @@ public class AudioManager { /** * Returns the minimum volume index for a particular stream. - * - * @param streamType The stream type whose minimum volume index is returned. + * @param streamType The stream type whose minimum volume index is returned. Must be one of + * {@link #STREAM_VOICE_CALL}, {@link #STREAM_SYSTEM}, + * {@link #STREAM_RING}, {@link #STREAM_MUSIC}, {@link #STREAM_ALARM}, + * {@link #STREAM_NOTIFICATION}, {@link #STREAM_DTMF} or {@link #STREAM_ACCESSIBILITY}. * @return The minimum valid volume index for the stream. * @see #getStreamVolume(int) - * @hide */ public int getStreamMinVolume(int streamType) { + if (!isPublicStreamType(streamType)) { + throw new IllegalArgumentException("Invalid stream type " + streamType); + } + return getStreamMinVolumeInt(streamType); + } + + /** + * @hide + * Same as {@link #getStreamMinVolume(int)} but without the check on the public stream type. + * @param streamType The stream type whose minimum volume index is returned. + * @return The minimum valid volume index for the stream. + * @see #getStreamVolume(int) + */ + public int getStreamMinVolumeInt(int streamType) { final IAudioService service = getService(); try { return service.getStreamMinVolume(streamType); @@ -944,6 +982,72 @@ public class AudioManager { } } + // keep in sync with frameworks/av/services/audiopolicy/common/include/Volume.h + private static final float VOLUME_MIN_DB = -758.0f; + + /** @hide */ + @IntDef(flag = false, prefix = "STREAM", value = { + STREAM_VOICE_CALL, + STREAM_SYSTEM, + STREAM_RING, + STREAM_MUSIC, + STREAM_ALARM, + STREAM_NOTIFICATION, + STREAM_DTMF, + STREAM_ACCESSIBILITY } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface PublicStreamTypes {} + + /** + * Returns the volume in dB (decibel) for the given stream type at the given volume index, on + * the given type of audio output device. + * @param streamType stream type for which the volume is queried. + * @param index the volume index for which the volume is queried. The index value must be + * between the minimum and maximum index values for the given stream type (see + * {@link #getStreamMinVolume(int)} and {@link #getStreamMaxVolume(int)}). + * @param deviceType the type of audio output device for which volume is queried. + * @return a volume expressed in dB. + * A negative value indicates the audio signal is attenuated. A typical maximum value + * at the maximum volume index is 0 dB (no attenuation nor amplification). Muting is + * reflected by a value of {@link Float#NEGATIVE_INFINITY}. + */ + public float getStreamVolumeDb(@PublicStreamTypes int streamType, int index, + @AudioDeviceInfo.AudioDeviceTypeOut int deviceType) { + if (!isPublicStreamType(streamType)) { + throw new IllegalArgumentException("Invalid stream type " + streamType); + } + if (index > getStreamMaxVolume(streamType) || index < getStreamMinVolume(streamType)) { + throw new IllegalArgumentException("Invalid stream volume index " + index); + } + if (!AudioDeviceInfo.isValidAudioDeviceTypeOut(deviceType)) { + throw new IllegalArgumentException("Invalid audio output device type " + deviceType); + } + final float gain = AudioSystem.getStreamVolumeDB(streamType, index, + AudioDeviceInfo.convertDeviceTypeToInternalDevice(deviceType)); + if (gain <= VOLUME_MIN_DB) { + return Float.NEGATIVE_INFINITY; + } else { + return gain; + } + } + + private static boolean isPublicStreamType(int streamType) { + switch (streamType) { + case STREAM_VOICE_CALL: + case STREAM_SYSTEM: + case STREAM_RING: + case STREAM_MUSIC: + case STREAM_ALARM: + case STREAM_NOTIFICATION: + case STREAM_DTMF: + case STREAM_ACCESSIBILITY: + return true; + default: + return false; + } + } + /** * Get last audible volume before stream was muted. * @@ -1246,9 +1350,22 @@ public class AudioManager { } //==================================================================== + // Offload query + /** + * Returns whether offloaded playback of an audio format is supported on the device. + * Offloaded playback is where the decoding of an audio stream is not competing with other + * software resources. In general, it is supported by dedicated hardware, such as audio DSPs. + * @param format the audio format (codec, sample rate, channels) being checked. + * @return true if the given audio format can be offloaded. + */ + public boolean isOffloadedPlaybackSupported(@NonNull AudioFormat format) { + return AudioSystem.isOffloadSupported(format); + } + + //==================================================================== // Bluetooth SCO control /** - * Sticky broadcast intent action indicating that the bluetoooth SCO audio + * Sticky broadcast intent action indicating that the Bluetooth SCO audio * connection state has changed. The intent contains on extra {@link #EXTRA_SCO_AUDIO_STATE} * indicating the new state which is either {@link #SCO_AUDIO_STATE_DISCONNECTED} * or {@link #SCO_AUDIO_STATE_CONNECTED} @@ -1262,7 +1379,7 @@ public class AudioManager { "android.media.SCO_AUDIO_STATE_CHANGED"; /** - * Sticky broadcast intent action indicating that the bluetoooth SCO audio + * Sticky broadcast intent action indicating that the Bluetooth SCO audio * connection state has been updated. * <p>This intent has two extras: * <ul> @@ -1552,6 +1669,21 @@ public class AudioManager { } /** + * Broadcast Action: microphone muting state changed. + * + * You <em>cannot</em> receive this through components declared + * in manifests, only by explicitly registering for it with + * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) + * Context.registerReceiver()}. + * + * <p>The intent has no extra values, use {@link #isMicrophoneMute} to check whether the + * microphone is muted. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MICROPHONE_MUTE_CHANGED = + "android.media.action.MICROPHONE_MUTE_CHANGED"; + + /** * Sets the audio mode. * <p> * The audio mode encompasses audio routing AND the behavior of @@ -1966,9 +2098,28 @@ public class AudioManager { */ private boolean querySoundEffectsEnabled(int user) { return Settings.System.getIntForUser(getContext().getContentResolver(), - Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0; + Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0 + && !areSystemSoundsZenModeBlocked(getContext()); } + private boolean areSystemSoundsZenModeBlocked(Context context) { + int zenMode = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.ZEN_MODE, 0); + + switch (zenMode) { + case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: + case Settings.Global.ZEN_MODE_ALARMS: + return true; + case Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + final NotificationManager noMan = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + return (noMan.getNotificationPolicy().priorityCategories + & NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER) == 0; + case Settings.Global.ZEN_MODE_OFF: + default: + return false; + } + } /** * Load Sound effects. @@ -2194,6 +2345,20 @@ public class AudioManager { } } } + + @Override + public void dispatchFocusResultFromExtPolicy(int requestResult, String clientId) { + synchronized (mFocusRequestsLock) { + // TODO use generation counter as the key instead + final BlockingFocusResultReceiver focusReceiver = + mFocusRequestsAwaitingResult.remove(clientId); + if (focusReceiver != null) { + focusReceiver.notifyResult(requestResult); + } else { + Log.e(TAG, "dispatchFocusResultFromExtPolicy found no result receiver"); + } + } + } }; private String getIdForAudioFocusListener(OnAudioFocusChangeListener l) { @@ -2246,6 +2411,40 @@ public class AudioManager { */ public static final int AUDIOFOCUS_REQUEST_DELAYED = 2; + /** @hide */ + @IntDef(flag = false, prefix = "AUDIOFOCUS_REQUEST", value = { + AUDIOFOCUS_REQUEST_FAILED, + AUDIOFOCUS_REQUEST_GRANTED, + AUDIOFOCUS_REQUEST_DELAYED } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface FocusRequestResult {} + + /** + * @hide + * code returned when a synchronous focus request on the client-side is to be blocked + * until the external audio focus policy decides on the response for the client + */ + public static final int AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY = 100; + + /** + * Timeout duration in ms when waiting on an external focus policy for the result for a + * focus request + */ + private static final int EXT_FOCUS_POLICY_TIMEOUT_MS = 200; + + private static final String FOCUS_CLIENT_ID_STRING = "android_audio_focus_client_id"; + + private final Object mFocusRequestsLock = new Object(); + /** + * Map of all receivers of focus request results, one per unresolved focus request. + * Receivers are added before sending the request to the external focus policy, + * and are removed either after receiving the result, or after the timeout. + * This variable is lazily initialized. + */ + @GuardedBy("mFocusRequestsLock") + private HashMap<String, BlockingFocusResultReceiver> mFocusRequestsAwaitingResult; + /** * Request audio focus. @@ -2512,18 +2711,100 @@ public class AudioManager { // some tests don't have a Context sdk = Build.VERSION.SDK_INT; } - try { - status = service.requestAudioFocus(afr.getAudioAttributes(), - afr.getFocusGain(), mICallBack, - mAudioFocusDispatcher, - getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener()), - getContext().getOpPackageName() /* package name */, afr.getFlags(), - ap != null ? ap.cb() : null, - sdk); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + + final String clientId = getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener()); + final BlockingFocusResultReceiver focusReceiver; + synchronized (mFocusRequestsLock) { + try { + // TODO status contains result and generation counter for ext policy + status = service.requestAudioFocus(afr.getAudioAttributes(), + afr.getFocusGain(), mICallBack, + mAudioFocusDispatcher, + clientId, + getContext().getOpPackageName() /* package name */, afr.getFlags(), + ap != null ? ap.cb() : null, + sdk); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + if (status != AudioManager.AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY) { + // default path with no external focus policy + return status; + } + if (mFocusRequestsAwaitingResult == null) { + mFocusRequestsAwaitingResult = + new HashMap<String, BlockingFocusResultReceiver>(1); + } + focusReceiver = new BlockingFocusResultReceiver(clientId); + mFocusRequestsAwaitingResult.put(clientId, focusReceiver); + } + focusReceiver.waitForResult(EXT_FOCUS_POLICY_TIMEOUT_MS); + if (DEBUG && !focusReceiver.receivedResult()) { + Log.e(TAG, "requestAudio response from ext policy timed out, denying request"); + } + synchronized (mFocusRequestsLock) { + mFocusRequestsAwaitingResult.remove(clientId); + } + return focusReceiver.requestResult(); + } + + // helper class that abstracts out the handling of spurious wakeups in Object.wait() + private static final class SafeWaitObject { + private boolean mQuit = false; + + public void safeNotify() { + synchronized (this) { + mQuit = true; + this.notify(); + } + } + + public void safeWait(long millis) throws InterruptedException { + final long timeOutTime = java.lang.System.currentTimeMillis() + millis; + synchronized (this) { + while (!mQuit) { + final long timeToWait = timeOutTime - java.lang.System.currentTimeMillis(); + if (timeToWait < 0) { break; } + this.wait(timeToWait); + } + } + } + } + + private static final class BlockingFocusResultReceiver { + private final SafeWaitObject mLock = new SafeWaitObject(); + @GuardedBy("mLock") + private boolean mResultReceived = false; + // request denied by default (e.g. timeout) + private int mFocusRequestResult = AudioManager.AUDIOFOCUS_REQUEST_FAILED; + private final String mFocusClientId; + + BlockingFocusResultReceiver(String clientId) { + mFocusClientId = clientId; + } + + boolean receivedResult() { return mResultReceived; } + int requestResult() { return mFocusRequestResult; } + + void notifyResult(int requestResult) { + synchronized (mLock) { + mResultReceived = true; + mFocusRequestResult = requestResult; + mLock.safeNotify(); + } + } + + public void waitForResult(long timeOutMs) { + synchronized (mLock) { + if (mResultReceived) { + // the result was received before waiting + return; + } + try { + mLock.safeWait(timeOutMs); + } catch (InterruptedException e) { } + } } - return status; } /** @@ -2570,6 +2851,32 @@ public class AudioManager { /** * @hide + * Set the result to the audio focus request received through + * {@link AudioPolicyFocusListener#onAudioFocusRequest(AudioFocusInfo, int)}. + * @param afi the information about the focus requester + * @param requestResult the result to the focus request to be passed to the requester + * @param ap a valid registered {@link AudioPolicy} configured as a focus policy. + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void setFocusRequestResult(@NonNull AudioFocusInfo afi, + @FocusRequestResult int requestResult, @NonNull AudioPolicy ap) { + if (afi == null) { + throw new IllegalArgumentException("Illegal null AudioFocusInfo"); + } + if (ap == null) { + throw new IllegalArgumentException("Illegal null AudioPolicy"); + } + final IAudioService service = getService(); + try { + service.setFocusRequestResultFromExtPolicy(afi, requestResult, ap.cb()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide * Notifies an application with a focus listener of gain or loss of audio focus. * This method can only be used by owners of an {@link AudioPolicy} configured with * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to true. @@ -2859,7 +3166,7 @@ public class AudioManager { final IAudioService service = getService(); try { String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(), - policy.hasFocusListener(), policy.isFocusPolicy()); + policy.hasFocusListener(), policy.isFocusPolicy(), policy.isVolumeController()); if (regId == null) { return ERROR; } else { @@ -4146,7 +4453,15 @@ public class AudioManager { Log.w(TAG, "updateAudioPortCache: listAudioPatches failed"); return status; } - } while (patchGeneration[0] != portGeneration[0]); + // Loop until patch generation is the same as port generation unless audio ports + // and audio patches are not null. + } while (patchGeneration[0] != portGeneration[0] + && (ports == null || patches == null)); + // If the patch generation doesn't equal port generation, return ERROR here in case + // of mismatch between audio ports and audio patches. + if (patchGeneration[0] != portGeneration[0]) { + return ERROR; + } for (int i = 0; i < newPatches.size(); i++) { for (int j = 0; j < newPatches.get(i).sources().length; j++) { @@ -4428,6 +4743,51 @@ public class AudioManager { } } + /** + * Set port id for microphones by matching device type and address. + * @hide + */ + public static void setPortIdForMicrophones(ArrayList<MicrophoneInfo> microphones) { + AudioDeviceInfo[] devices = getDevicesStatic(AudioManager.GET_DEVICES_INPUTS); + for (int i = microphones.size() - 1; i >= 0; i--) { + boolean foundPortId = false; + for (AudioDeviceInfo device : devices) { + if (device.getPort().type() == microphones.get(i).getInternalDeviceType() + && TextUtils.equals(device.getAddress(), microphones.get(i).getAddress())) { + microphones.get(i).setId(device.getId()); + foundPortId = true; + break; + } + } + if (!foundPortId) { + Log.i(TAG, "Failed to find port id for device with type:" + + microphones.get(i).getType() + " address:" + + microphones.get(i).getAddress()); + microphones.remove(i); + } + } + } + + /** + * Returns a list of {@link MicrophoneInfo} that corresponds to the characteristics + * of all available microphones. The list is empty when no microphones are available + * on the device. An error during the query will result in an IOException being thrown. + * + * @return a list that contains all microphones' characteristics + * @throws IOException if an error occurs. + */ + public List<MicrophoneInfo> getMicrophones() throws IOException { + ArrayList<MicrophoneInfo> microphones = new ArrayList<MicrophoneInfo>(); + int status = AudioSystem.getMicrophones(microphones); + if (status != AudioManager.SUCCESS) { + // fail and bail! + Log.e(TAG, "getMicrophones failed:" + status); + return new ArrayList<MicrophoneInfo>(); // Always return a list. + } + setPortIdForMicrophones(microphones); + return microphones; + } + // Since we need to calculate the changes since THE LAST NOTIFICATION, and not since the // (unpredictable) last time updateAudioPortCache() was called by someone, keep a list // of the ports that exist at the time of the last notification. @@ -4507,6 +4867,114 @@ public class AudioManager { } } + + /** + * @hide + * Abstract class to receive event notification about audioserver process state. + */ + @SystemApi + public abstract static class AudioServerStateCallback { + public void onAudioServerDown() { } + public void onAudioServerUp() { } + } + + private Executor mAudioServerStateExec; + private AudioServerStateCallback mAudioServerStateCb; + private final Object mAudioServerStateCbLock = new Object(); + + private final IAudioServerStateDispatcher mAudioServerStateDispatcher = + new IAudioServerStateDispatcher.Stub() { + @Override + public void dispatchAudioServerStateChange(boolean state) { + Executor exec; + AudioServerStateCallback cb; + + synchronized (mAudioServerStateCbLock) { + exec = mAudioServerStateExec; + cb = mAudioServerStateCb; + } + + if ((exec == null) || (cb == null)) { + return; + } + if (state) { + exec.execute(() -> cb.onAudioServerUp()); + } else { + exec.execute(() -> cb.onAudioServerDown()); + } + } + }; + + /** + * @hide + * Registers a callback for notification of audio server state changes. + * @param executor {@link Executor} to handle the callbacks + * @param stateCallback the callback to receive the audio server state changes + * To remove the callabck, pass a null reference for both executor and stateCallback. + */ + @SystemApi + public void setAudioServerStateCallback(@NonNull Executor executor, + @NonNull AudioServerStateCallback stateCallback) { + if (stateCallback == null) { + throw new IllegalArgumentException("Illegal null AudioServerStateCallback"); + } + if (executor == null) { + throw new IllegalArgumentException( + "Illegal null Executor for the AudioServerStateCallback"); + } + + synchronized (mAudioServerStateCbLock) { + if (mAudioServerStateCb != null) { + throw new IllegalStateException( + "setAudioServerStateCallback called with already registered callabck"); + } + final IAudioService service = getService(); + try { + service.registerAudioServerStateDispatcher(mAudioServerStateDispatcher); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mAudioServerStateExec = executor; + mAudioServerStateCb = stateCallback; + } + } + + /** + * @hide + * Unregisters the callback for notification of audio server state changes. + */ + @SystemApi + public void clearAudioServerStateCallback() { + synchronized (mAudioServerStateCbLock) { + if (mAudioServerStateCb != null) { + final IAudioService service = getService(); + try { + service.unregisterAudioServerStateDispatcher( + mAudioServerStateDispatcher); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + mAudioServerStateExec = null; + mAudioServerStateCb = null; + } + } + + /** + * @hide + * Checks if native audioservice is running or not. + * @return true if native audioservice runs, false otherwise. + */ + @SystemApi + public boolean isAudioServerRunning() { + final IAudioService service = getService(); + try { + return service.isAudioServerRunning(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + //--------------------------------------------------------- // Inner classes //-------------------- diff --git a/media/java/android/media/AudioPort.java b/media/java/android/media/AudioPort.java index 19bf51d982eb..047db19431c2 100644 --- a/media/java/android/media/AudioPort.java +++ b/media/java/android/media/AudioPort.java @@ -20,7 +20,7 @@ package android.media; * An audio port is a node of the audio framework or hardware that can be connected to or * disconnect from another audio node to create a specific audio routing configuration. * Examples of audio ports are an output device (speaker) or an output mix (see AudioMixPort). - * All attributes that are relevant for applications to make routing selection are decribed + * All attributes that are relevant for applications to make routing selection are described * in an AudioPort, in particular: * - possible channel mask configurations. * - audio format (PCM 16bit, PCM 24bit...) @@ -173,6 +173,7 @@ public class AudioPort { /** * Build a specific configuration of this audio port for use by methods * like AudioManager.connectAudioPatch(). + * @param samplingRate * @param channelMask The desired channel mask. AudioFormat.CHANNEL_OUT_DEFAULT if no change * from active configuration requested. * @param format The desired audio format. AudioFormat.ENCODING_DEFAULT if no change diff --git a/media/java/android/media/AudioPortEventHandler.java b/media/java/android/media/AudioPortEventHandler.java index c152245d4f9a..ac3904a22882 100644 --- a/media/java/android/media/AudioPortEventHandler.java +++ b/media/java/android/media/AudioPortEventHandler.java @@ -17,6 +17,7 @@ package android.media; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import java.util.ArrayList; @@ -30,6 +31,7 @@ import java.lang.ref.WeakReference; class AudioPortEventHandler { private Handler mHandler; + private HandlerThread mHandlerThread; private final ArrayList<AudioManager.OnAudioPortUpdateListener> mListeners = new ArrayList<AudioManager.OnAudioPortUpdateListener>(); @@ -40,6 +42,8 @@ class AudioPortEventHandler { private static final int AUDIOPORT_EVENT_SERVICE_DIED = 3; private static final int AUDIOPORT_EVENT_NEW_LISTENER = 4; + private static final long RESCHEDULE_MESSAGE_DELAY_MS = 100; + /** * Accessed by native methods: JNI Callback context. */ @@ -51,11 +55,12 @@ class AudioPortEventHandler { if (mHandler != null) { return; } - // find the looper for our new event handler - Looper looper = Looper.getMainLooper(); + // create a new thread for our new event handler + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); - if (looper != null) { - mHandler = new Handler(looper) { + if (mHandlerThread.getLooper() != null) { + mHandler = new Handler(mHandlerThread.getLooper()) { @Override public void handleMessage(Message msg) { ArrayList<AudioManager.OnAudioPortUpdateListener> listeners; @@ -86,6 +91,12 @@ class AudioPortEventHandler { if (msg.what != AUDIOPORT_EVENT_SERVICE_DIED) { int status = AudioManager.updateAudioPortCache(ports, patches, null); if (status != AudioManager.SUCCESS) { + // Since audio ports and audio patches are not null, the return + // value could be ERROR due to inconsistency between port generation + // and patch generation. In this case, we need to reschedule the + // message to make sure the native callback is done. + sendMessageDelayed(obtainMessage(msg.what, msg.obj), + RESCHEDULE_MESSAGE_DELAY_MS); return; } } @@ -132,6 +143,9 @@ class AudioPortEventHandler { @Override protected void finalize() { native_finalize(); + if (mHandlerThread.isAlive()) { + mHandlerThread.quit(); + } } private native void native_finalize(); @@ -168,6 +182,10 @@ class AudioPortEventHandler { Handler handler = eventHandler.handler(); if (handler != null) { Message m = handler.obtainMessage(what, arg1, arg2, obj); + if (what != AUDIOPORT_EVENT_NEW_LISTENER) { + // Except AUDIOPORT_EVENT_NEW_LISTENER, we can only respect the last message. + handler.removeMessages(what); + } handler.sendMessage(m); } } diff --git a/media/java/android/media/AudioPresentation.java b/media/java/android/media/AudioPresentation.java new file mode 100644 index 000000000000..4652c180936c --- /dev/null +++ b/media/java/android/media/AudioPresentation.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2018 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.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + + +/** + * The AudioPresentation class encapsulates the information that describes an audio presentation + * which is available in next generation audio content. + * + * Used by {@link MediaExtractor} {@link MediaExtractor#getAudioPresentations(int)} and + * {@link AudioTrack} {@link AudioTrack#setPresentation(AudioPresentation)} to query available + * presentations and to select one. + * + * A list of available audio presentations in a media source can be queried using + * {@link MediaExtractor#getAudioPresentations(int)}. This list can be presented to a user for + * selection. + * An AudioPresentation can be passed to an offloaded audio decoder via + * {@link AudioTrack#setPresentation(AudioPresentation)} to request decoding of the selected + * presentation. An audio stream may contain multiple presentations that differ by language, + * accessibility, end point mastering and dialogue enhancement. An audio presentation may also have + * a set of description labels in different languages to help the user to make an informed + * selection. + */ +public final class AudioPresentation { + private final int mPresentationId; + private final int mProgramId; + private final Map<String, String> mLabels; + private final String mLanguage; + + /** @hide */ + @IntDef( + value = { + MASTERING_NOT_INDICATED, + MASTERED_FOR_STEREO, + MASTERED_FOR_SURROUND, + MASTERED_FOR_3D, + MASTERED_FOR_HEADPHONE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface MasteringIndicationType {} + + private final @MasteringIndicationType int mMasteringIndication; + private final boolean mAudioDescriptionAvailable; + private final boolean mSpokenSubtitlesAvailable; + private final boolean mDialogueEnhancementAvailable; + + /** + * No preferred reproduction channel layout. + */ + public static final int MASTERING_NOT_INDICATED = 0; + /** + * Stereo speaker layout. + */ + public static final int MASTERED_FOR_STEREO = 1; + /** + * Two-dimensional (e.g. 5.1) speaker layout. + */ + public static final int MASTERED_FOR_SURROUND = 2; + /** + * Three-dimensional (e.g. 5.1.2) speaker layout. + */ + public static final int MASTERED_FOR_3D = 3; + /** + * Prerendered for headphone playback. + */ + public static final int MASTERED_FOR_HEADPHONE = 4; + + AudioPresentation(int presentationId, + int programId, + Map<String, String> labels, + String language, + @MasteringIndicationType int masteringIndication, + boolean audioDescriptionAvailable, + boolean spokenSubtitlesAvailable, + boolean dialogueEnhancementAvailable) { + this.mPresentationId = presentationId; + this.mProgramId = programId; + this.mLanguage = language; + this.mMasteringIndication = masteringIndication; + this.mAudioDescriptionAvailable = audioDescriptionAvailable; + this.mSpokenSubtitlesAvailable = spokenSubtitlesAvailable; + this.mDialogueEnhancementAvailable = dialogueEnhancementAvailable; + + this.mLabels = new HashMap<String, String>(labels); + } + + /** + * The framework uses this presentation id to select an audio presentation rendered by a + * decoder. Presentation id is typically sequential, but does not have to be. + * @hide + */ + public int getPresentationId() { + return mPresentationId; + } + + /** + * The framework uses this program id to select an audio presentation rendered by a decoder. + * Program id can be used to further uniquely identify the presentation to a decoder. + * @hide + */ + public int getProgramId() { + return mProgramId; + } + + /** + * @return a map of available text labels for this presentation. Each label is indexed by its + * locale corresponding to the language code as specified by ISO 639-2 [42]. Either ISO 639-2/B + * or ISO 639-2/T could be used. + */ + public Map<Locale, String> getLabels() { + Map<Locale, String> localeLabels = new HashMap<>(); + for (Map.Entry<String, String> entry : mLabels.entrySet()) { + localeLabels.put(new Locale(entry.getKey()), entry.getValue()); + } + return localeLabels; + } + + /** + * @return the locale corresponding to audio presentation's ISO 639-1/639-2 language code. + */ + public Locale getLocale() { + return new Locale(mLanguage); + } + + /** + * @return the mastering indication of the audio presentation. + * See {@link #MASTERING_NOT_INDICATED}, {@link #MASTERED_FOR_STEREO}, + * {@link #MASTERED_FOR_SURROUND}, {@link #MASTERED_FOR_3D}, {@link #MASTERED_FOR_HEADPHONE} + */ + @MasteringIndicationType + public int getMasteringIndication() { + return mMasteringIndication; + } + + /** + * Indicates whether an audio description for the visually impaired is available. + * @return {@code true} if audio description is available. + */ + public boolean hasAudioDescription() { + return mAudioDescriptionAvailable; + } + + /** + * Indicates whether spoken subtitles for the visually impaired are available. + * @return {@code true} if spoken subtitles are available. + */ + public boolean hasSpokenSubtitles() { + return mSpokenSubtitlesAvailable; + } + + /** + * Indicates whether dialogue enhancement is available. + * @return {@code true} if dialogue enhancement is available. + */ + public boolean hasDialogueEnhancement() { + return mDialogueEnhancementAvailable; + } +} diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index 0906ba50f7df..384753018bc8 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -16,12 +16,15 @@ package android.media; +import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.Collection; import java.util.Iterator; +import java.util.ArrayList; +import java.util.List; import android.annotation.IntDef; import android.annotation.NonNull; @@ -32,8 +35,10 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.PersistableBundle; import android.os.RemoteException; import android.os.ServiceManager; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -1314,6 +1319,23 @@ public class AudioRecord implements AudioRouting return native_read_in_direct_buffer(audioBuffer, sizeInBytes, readMode == READ_BLOCKING); } + /** + * Return Metrics data about the current AudioTrack instance. + * + * @return a {@link PersistableBundle} containing the set of attributes and values + * available for the media being handled by this instance of AudioRecord + * The attributes are descibed in {@link MetricsConstants}. + * + * Additional vendor-specific fields may also be present in + * the return value. + */ + public PersistableBundle getMetrics() { + PersistableBundle bundle = native_getMetrics(); + return bundle; + } + + private native PersistableBundle native_getMetrics(); + //-------------------------------------------------------------------------- // Initialization / configuration //-------------------- @@ -1394,6 +1416,7 @@ public class AudioRecord implements AudioRouting /* * Call BEFORE adding a routing callback handler. */ + @GuardedBy("mRoutingChangeListeners") private void testEnableNativeRoutingCallbacksLocked() { if (mRoutingChangeListeners.size() == 0) { native_enableDeviceCallback(); @@ -1403,6 +1426,7 @@ public class AudioRecord implements AudioRouting /* * Call AFTER removing a routing callback handler. */ + @GuardedBy("mRoutingChangeListeners") private void testDisableNativeRoutingCallbacksLocked() { if (mRoutingChangeListeners.size() == 0) { native_disableDeviceCallback(); @@ -1516,66 +1540,13 @@ public class AudioRecord implements AudioRouting } /** - * Helper class to handle the forwarding of native events to the appropriate listener - * (potentially) handled in a different thread - */ - private class NativeRoutingEventHandlerDelegate { - private final Handler mHandler; - - NativeRoutingEventHandlerDelegate(final AudioRecord record, - final AudioRouting.OnRoutingChangedListener listener, - Handler handler) { - // find the looper for our new event handler - Looper looper; - if (handler != null) { - looper = handler.getLooper(); - } else { - // no given handler, use the looper the AudioRecord was created in - looper = mInitializationLooper; - } - - // construct the event handler with this looper - if (looper != null) { - // implement the event handler delegate - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - if (record == null) { - return; - } - switch(msg.what) { - case AudioSystem.NATIVE_EVENT_ROUTING_CHANGE: - if (listener != null) { - listener.onRoutingChanged(record); - } - break; - default: - loge("Unknown native event type: " + msg.what); - break; - } - } - }; - } else { - mHandler = null; - } - } - - Handler getHandler() { - return mHandler; - } - } - - /** * Sends device list change notification to all listeners. */ private void broadcastRoutingChange() { AudioManager.resetAudioPortGeneration(); synchronized (mRoutingChangeListeners) { for (NativeRoutingEventHandlerDelegate delegate : mRoutingChangeListeners.values()) { - Handler handler = delegate.getHandler(); - if (handler != null) { - handler.sendEmptyMessage(AudioSystem.NATIVE_EVENT_ROUTING_CHANGE); - } + delegate.notifyClient(); } } } @@ -1636,6 +1607,32 @@ public class AudioRecord implements AudioRouting } } + //-------------------------------------------------------------------------- + // Microphone information + //-------------------- + /** + * Returns a lists of {@link MicrophoneInfo} representing the active microphones. + * By querying channel mapping for each active microphone, developer can know how + * the microphone is used by each channels or a capture stream. + * Note that the information about the active microphones may change during a recording. + * See {@link AudioManager#registerAudioDeviceCallback} to be notified of changes + * in the audio devices, querying the active microphones then will return the latest + * information. + * + * @return a lists of {@link MicrophoneInfo} representing the active microphones. + * @throws IOException if an error occurs + */ + public List<MicrophoneInfo> getActiveMicrophones() throws IOException { + ArrayList<MicrophoneInfo> activeMicrophones = new ArrayList<>(); + int status = native_get_active_microphones(activeMicrophones); + if (status != AudioManager.SUCCESS) { + Log.e(TAG, "getActiveMicrophones failed:" + status); + return new ArrayList<MicrophoneInfo>(); + } + AudioManager.setPortIdForMicrophones(activeMicrophones); + return activeMicrophones; + } + //--------------------------------------------------------- // Interface definitions //-------------------- @@ -1781,6 +1778,9 @@ public class AudioRecord implements AudioRouting private native final int native_get_timestamp(@NonNull AudioTimestamp outTimestamp, @AudioTimestamp.Timebase int timebase); + private native final int native_get_active_microphones( + ArrayList<MicrophoneInfo> activeMicrophones); + //--------------------------------------------------------- // Utility methods //------------------ @@ -1792,4 +1792,46 @@ public class AudioRecord implements AudioRouting private static void loge(String msg) { Log.e(TAG, msg); } + + public static final class MetricsConstants + { + private MetricsConstants() {} + + /** + * Key to extract the output format being recorded + * from the {@link AudioRecord#getMetrics} return value. + * The value is a String. + */ + public static final String ENCODING = "android.media.audiorecord.encoding"; + + /** + * Key to extract the Source Type for this track + * from the {@link AudioRecord#getMetrics} return value. + * The value is a String. + */ + public static final String SOURCE = "android.media.audiorecord.source"; + + /** + * Key to extract the estimated latency through the recording pipeline + * from the {@link AudioRecord#getMetrics} return value. + * This is in units of milliseconds. + * The value is an integer. + */ + public static final String LATENCY = "android.media.audiorecord.latency"; + + /** + * Key to extract the sink sample rate for this record track in Hz + * from the {@link AudioRecord#getMetrics} return value. + * The value is an integer. + */ + public static final String SAMPLERATE = "android.media.audiorecord.samplerate"; + + /** + * Key to extract the number of channels being recorded in this record track + * from the {@link AudioRecord#getMetrics} return value. + * The value is an integer. + */ + public static final String CHANNELS = "android.media.audiorecord.channels"; + + } } diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index e56944dff782..be9fcb8fae83 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -16,6 +16,7 @@ package android.media; +import android.annotation.NonNull; import android.content.Context; import android.content.pm.PackageManager; import android.media.audiopolicy.AudioMix; @@ -792,7 +793,7 @@ public class AudioSystem public static native int getPrimaryOutputFrameCount(); public static native int getOutputLatency(int stream); - public static native int setLowRamDevice(boolean isLowRamDevice); + public static native int setLowRamDevice(boolean isLowRamDevice, long totalMemory); public static native int checkAudioFlinger(); public static native int listAudioPorts(ArrayList<AudioPort> ports, int[] generation); @@ -818,6 +819,16 @@ public class AudioSystem public static native float getStreamVolumeDB(int stream, int index, int device); + static boolean isOffloadSupported(@NonNull AudioFormat format) { + return native_is_offload_supported(format.getEncoding(), format.getSampleRate(), + format.getChannelMask(), format.getChannelIndexMask()); + } + + private static native boolean native_is_offload_supported(int encoding, int sampleRate, + int channelMask, int channelIndexMask); + + public static native int getMicrophones(ArrayList<MicrophoneInfo> microphonesInfo); + // Items shared with audio service /** @@ -914,7 +925,8 @@ public class AudioSystem (1 << STREAM_MUSIC) | (1 << STREAM_RING) | (1 << STREAM_NOTIFICATION) | - (1 << STREAM_SYSTEM); + (1 << STREAM_SYSTEM) | + (1 << STREAM_VOICE_CALL); /** * Event posted by AudioTrack and AudioRecord JNI (JNIDeviceCallback) when routing changes. diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 50145f8a9886..2d5fad5dde5b 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -24,7 +24,9 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.NioUtils; import java.util.Collection; +import java.util.concurrent.Executor; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -34,6 +36,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; @@ -185,6 +188,22 @@ public class AudioTrack extends PlayerBase * Event id denotes when previously set update period has elapsed during playback. */ private static final int NATIVE_EVENT_NEW_POS = 4; + /** + * Callback for more data + * TODO only for offload + */ + private static final int NATIVE_EVENT_MORE_DATA = 0; + /** + * IAudioTrack tear down for offloaded tracks + * TODO: when received, java AudioTrack must be released + */ + private static final int NATIVE_EVENT_NEW_IAUDIOTRACK = 6; + /** + * Event id denotes when all the buffers queued in AF and HW are played + * back (after stop is called) for an offloaded track. + * TODO: not just for offload + */ + private static final int NATIVE_EVENT_STREAM_END = 7; private final static String TAG = "android.media.AudioTrack"; @@ -540,6 +559,12 @@ public class AudioTrack extends PlayerBase public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId) throws IllegalArgumentException { + this(attributes, format, bufferSizeInBytes, mode, sessionId, false /*offload*/); + } + + private AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, + int mode, int sessionId, boolean offload) + throws IllegalArgumentException { super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK); // mState already == STATE_UNINITIALIZED @@ -601,7 +626,8 @@ public class AudioTrack extends PlayerBase // native initialization int initResult = native_setup(new WeakReference<AudioTrack>(this), mAttributes, sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat, - mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/); + mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/, + offload); if (initResult != SUCCESS) { loge("Error code "+initResult+" when initializing AudioTrack."); return; // with mState == STATE_UNINITIALIZED @@ -681,7 +707,8 @@ public class AudioTrack extends PlayerBase 0 /*mNativeBufferSizeInBytes - NA*/, 0 /*mDataLoadMode - NA*/, session, - nativeTrackInJavaObj); + nativeTrackInJavaObj, + false /*offload*/); if (initResult != SUCCESS) { loge("Error code "+initResult+" when initializing AudioTrack."); return; // with mState == STATE_UNINITIALIZED @@ -729,6 +756,7 @@ public class AudioTrack extends PlayerBase * <code>MODE_STREAM</code> will be used. * <br>If the session ID is not specified with {@link #setSessionId(int)}, a new one will * be generated. + * <br>Offload is false by default. */ public static class Builder { private AudioAttributes mAttributes; @@ -737,6 +765,7 @@ public class AudioTrack extends PlayerBase private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE; private int mMode = MODE_STREAM; private int mPerformanceMode = PERFORMANCE_MODE_NONE; + private boolean mOffload = false; /** * Constructs a new Builder with the default values as described above. @@ -867,6 +896,21 @@ public class AudioTrack extends PlayerBase } /** + * Sets whether this track will play through the offloaded audio path. + * When set to true, at build time, the audio format will be checked against + * {@link AudioManager#isOffloadedPlaybackSupported(AudioFormat)} to verify the audio format + * used by this track is supported on the device's offload path (if any). + * <br>Offload is only supported for media audio streams, and therefore requires that + * the usage be {@link AudioAttributes#USAGE_MEDIA}. + * @param offload true to require the offload path for playback. + * @return the same Builder instance. + */ + public @NonNull Builder setOffloadedPlayback(boolean offload) { + mOffload = offload; + return this; + } + + /** * Builds an {@link AudioTrack} instance initialized with all the parameters set * on this <code>Builder</code>. * @return a new successfully initialized {@link AudioTrack} instance. @@ -909,6 +953,19 @@ public class AudioTrack extends PlayerBase .setEncoding(AudioFormat.ENCODING_DEFAULT) .build(); } + + //TODO tie offload to PERFORMANCE_MODE_POWER_SAVING? + if (mOffload) { + if (mAttributes.getUsage() != AudioAttributes.USAGE_MEDIA) { + throw new UnsupportedOperationException( + "Cannot create AudioTrack, offload requires USAGE_MEDIA"); + } + if (!AudioSystem.isOffloadSupported(mFormat)) { + throw new UnsupportedOperationException( + "Cannot create AudioTrack, offload format not supported"); + } + } + try { // If the buffer size is not specified in streaming mode, // use a single frame for the buffer size and let the @@ -918,7 +975,7 @@ public class AudioTrack extends PlayerBase * mFormat.getBytesPerSample(mFormat.getEncoding()); } final AudioTrack track = new AudioTrack( - mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId); + mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId, mOffload); if (track.getState() == STATE_UNINITIALIZED) { // release is not necessary throw new UnsupportedOperationException("Cannot create AudioTrack"); @@ -1662,6 +1719,23 @@ public class AudioTrack extends PlayerBase return ret; } + /** + * Return Metrics data about the current AudioTrack instance. + * + * @return a {@link PersistableBundle} containing the set of attributes and values + * available for the media being handled by this instance of AudioTrack + * The attributes are descibed in {@link MetricsConstants}. + * + * Additional vendor-specific fields may also be present in + * the return value. + */ + public PersistableBundle getMetrics() { + PersistableBundle bundle = native_getMetrics(); + return bundle; + } + + private native PersistableBundle native_getMetrics(); + //-------------------------------------------------------------------------- // Initialization / configuration //-------------------- @@ -1934,6 +2008,25 @@ public class AudioTrack extends PlayerBase } /** + * Sets the audio presentation. + * If the audio presentation is invalid then {@link #ERROR_BAD_VALUE} will be returned. + * If a multi-stream decoder (MSD) is not present, or the format does not support + * multiple presentations, then {@link #ERROR_INVALID_OPERATION} will be returned. + * @param presentation see {@link AudioPresentation}. In particular, id should be set. + * @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE}, + * {@link #ERROR_INVALID_OPERATION} + * @throws IllegalArgumentException if the audio presentation is null. + * @throws IllegalStateException if track is not initialized. + */ + public int setPresentation(@NonNull AudioPresentation presentation) { + if (presentation == null) { + throw new IllegalArgumentException("audio presentation is null"); + } + return native_setPresentation(presentation.getPresentationId(), + presentation.getProgramId()); + } + + /** * Sets the initialization state of the instance. This method was originally intended to be used * in an AudioTrack subclass constructor to set a subclass-specific post-initialization state. * However, subclasses of AudioTrack are no longer recommended, so this method is obsolete. @@ -2728,6 +2821,7 @@ public class AudioTrack extends PlayerBase /* * Call BEFORE adding a routing callback handler. */ + @GuardedBy("mRoutingChangeListeners") private void testEnableNativeRoutingCallbacksLocked() { if (mRoutingChangeListeners.size() == 0) { native_enableDeviceCallback(); @@ -2737,6 +2831,7 @@ public class AudioTrack extends PlayerBase /* * Call AFTER removing a routing callback handler. */ + @GuardedBy("mRoutingChangeListeners") private void testDisableNativeRoutingCallbacksLocked() { if (mRoutingChangeListeners.size() == 0) { native_disableDeviceCallback(); @@ -2856,10 +2951,7 @@ public class AudioTrack extends PlayerBase AudioManager.resetAudioPortGeneration(); synchronized (mRoutingChangeListeners) { for (NativeRoutingEventHandlerDelegate delegate : mRoutingChangeListeners.values()) { - Handler handler = delegate.getHandler(); - if (handler != null) { - handler.sendEmptyMessage(AudioSystem.NATIVE_EVENT_ROUTING_CHANGE); - } + delegate.notifyClient(); } } } @@ -2885,6 +2977,69 @@ public class AudioTrack extends PlayerBase void onPeriodicNotification(AudioTrack track); } + /** + * Abstract class to receive event notification about the stream playback. + * See {@link AudioTrack#setStreamEventCallback(Executor, StreamEventCallback)} to register + * the callback on the given {@link AudioTrack} instance. + */ + public abstract static class StreamEventCallback { + /** @hide */ // add hidden empty constructor so it doesn't show in SDK + public StreamEventCallback() { } + /** + * Called when an offloaded track is no longer valid and has been discarded by the system. + * An example of this happening is when an offloaded track has been paused too long, and + * gets invalidated by the system to prevent any other offload. + * @param track the {@link AudioTrack} on which the event happened + */ + public void onTearDown(AudioTrack track) { } + /** + * Called when all the buffers of an offloaded track that were queued in the audio system + * (e.g. the combination of the Android audio framework and the device's audio hardware) + * have been played after {@link AudioTrack#stop()} has been called. + * @param track the {@link AudioTrack} on which the event happened + */ + public void onStreamPresentationEnd(AudioTrack track) { } + /** + * Called when more audio data can be written without blocking on an offloaded track. + * @param track the {@link AudioTrack} on which the event happened + */ + public void onStreamDataRequest(AudioTrack track) { } + } + + private Executor mStreamEventExec; + private StreamEventCallback mStreamEventCb; + private final Object mStreamEventCbLock = new Object(); + + /** + * Sets the callback for the notification of stream events. + * @param executor {@link Executor} to handle the callbacks + * @param eventCallback the callback to receive the stream event notifications + */ + public void setStreamEventCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull StreamEventCallback eventCallback) { + if (eventCallback == null) { + throw new IllegalArgumentException("Illegal null StreamEventCallback"); + } + if (executor == null) { + throw new IllegalArgumentException("Illegal null Executor for the StreamEventCallback"); + } + synchronized (mStreamEventCbLock) { + mStreamEventExec = executor; + mStreamEventCb = eventCallback; + } + } + + /** + * Unregisters the callback for notification of stream events, previously set + * by {@link #setStreamEventCallback(Executor, StreamEventCallback)}. + */ + public void removeStreamEventCallback() { + synchronized (mStreamEventCbLock) { + mStreamEventExec = null; + mStreamEventCb = null; + } + } + //--------------------------------------------------------- // Inner classes //-------------------- @@ -2943,56 +3098,6 @@ public class AudioTrack extends PlayerBase } } - /** - * Helper class to handle the forwarding of native events to the appropriate listener - * (potentially) handled in a different thread - */ - private class NativeRoutingEventHandlerDelegate { - private final Handler mHandler; - - NativeRoutingEventHandlerDelegate(final AudioTrack track, - final AudioRouting.OnRoutingChangedListener listener, - Handler handler) { - // find the looper for our new event handler - Looper looper; - if (handler != null) { - looper = handler.getLooper(); - } else { - // no given handler, use the looper the AudioTrack was created in - looper = mInitializationLooper; - } - - // construct the event handler with this looper - if (looper != null) { - // implement the event handler delegate - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - if (track == null) { - return; - } - switch(msg.what) { - case AudioSystem.NATIVE_EVENT_ROUTING_CHANGE: - if (listener != null) { - listener.onRoutingChanged(track); - } - break; - default: - loge("Unknown native event type: " + msg.what); - break; - } - } - }; - } else { - mHandler = null; - } - } - - Handler getHandler() { - return mHandler; - } - } - //--------------------------------------------------------- // Methods for IPlayer interface //-------------------- @@ -3018,7 +3123,7 @@ public class AudioTrack extends PlayerBase private static void postEventFromNative(Object audiotrack_ref, int what, int arg1, int arg2, Object obj) { //logd("Event posted from the native side: event="+ what + " args="+ arg1+" "+arg2); - AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get(); + final AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get(); if (track == null) { return; } @@ -3027,6 +3132,32 @@ public class AudioTrack extends PlayerBase track.broadcastRoutingChange(); return; } + + if (what == NATIVE_EVENT_MORE_DATA || what == NATIVE_EVENT_NEW_IAUDIOTRACK + || what == NATIVE_EVENT_STREAM_END) { + final Executor exec; + final StreamEventCallback cb; + synchronized (track.mStreamEventCbLock) { + exec = track.mStreamEventExec; + cb = track.mStreamEventCb; + } + if ((exec == null) || (cb == null)) { + return; + } + switch (what) { + case NATIVE_EVENT_MORE_DATA: + exec.execute(() -> cb.onStreamDataRequest(track)); + return; + case NATIVE_EVENT_NEW_IAUDIOTRACK: + // TODO also release track as it's not longer usable + exec.execute(() -> cb.onTearDown(track)); + return; + case NATIVE_EVENT_STREAM_END: + exec.execute(() -> cb.onStreamPresentationEnd(track)); + return; + } + } + NativePositionEventHandlerDelegate delegate = track.mEventHandlerDelegate; if (delegate != null) { Handler handler = delegate.getHandler(); @@ -3048,7 +3179,8 @@ public class AudioTrack extends PlayerBase private native final int native_setup(Object /*WeakReference<AudioTrack>*/ audiotrack_this, Object /*AudioAttributes*/ attributes, int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, - int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack); + int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack, + boolean offload); private native final void native_finalize(); @@ -3134,6 +3266,7 @@ public class AudioTrack extends PlayerBase @NonNull VolumeShaper.Operation operation); private native @Nullable VolumeShaper.State native_getVolumeShaperState(int id); + private native final int native_setPresentation(int presentationId, int programId); //--------------------------------------------------------- // Utility methods @@ -3146,4 +3279,46 @@ public class AudioTrack extends PlayerBase private static void loge(String msg) { Log.e(TAG, msg); } + + public final static class MetricsConstants + { + private MetricsConstants() {} + + /** + * Key to extract the Stream Type for this track + * from the {@link AudioTrack#getMetrics} return value. + * The value is a String. + */ + public static final String STREAMTYPE = "android.media.audiotrack.streamtype"; + + /** + * Key to extract the Content Type for this track + * from the {@link AudioTrack#getMetrics} return value. + * The value is a String. + */ + public static final String CONTENTTYPE = "android.media.audiotrack.type"; + + /** + * Key to extract the Content Type for this track + * from the {@link AudioTrack#getMetrics} return value. + * The value is a String. + */ + public static final String USAGE = "android.media.audiotrack.usage"; + + /** + * Key to extract the sample rate for this track in Hz + * from the {@link AudioTrack#getMetrics} return value. + * The value is an integer. + */ + public static final String SAMPLERATE = "android.media.audiorecord.samplerate"; + + /** + * Key to extract the channel mask information for this track + * from the {@link AudioTrack#getMetrics} return value. + * + * The value is a Long integer. + */ + public static final String CHANNELMASK = "android.media.audiorecord.channelmask"; + + } } diff --git a/media/java/android/media/BufferingParams.java b/media/java/android/media/BufferingParams.java index 681271b1a6f0..521e89756f5d 100644 --- a/media/java/android/media/BufferingParams.java +++ b/media/java/android/media/BufferingParams.java @@ -26,170 +26,68 @@ import java.lang.annotation.RetentionPolicy; /** * Structure for source buffering management params. * - * Used by {@link MediaPlayer#getDefaultBufferingParams()}, - * {@link MediaPlayer#getBufferingParams()} and + * Used by {@link MediaPlayer#getBufferingParams()} and * {@link MediaPlayer#setBufferingParams(BufferingParams)} * to control source buffering behavior. * * <p>There are two stages of source buffering in {@link MediaPlayer}: initial buffering * (when {@link MediaPlayer} is being prepared) and rebuffering (when {@link MediaPlayer} - * is playing back source). {@link BufferingParams} includes mode and corresponding - * watermarks for each stage of source buffering. The watermarks could be either size - * based (in milliseconds), or time based (in kilobytes) or both, depending on the mode. + * is playing back source). {@link BufferingParams} includes corresponding marks for each + * stage of source buffering. The marks are time based (in milliseconds). * - * <p>There are 4 buffering modes: {@link #BUFFERING_MODE_NONE}, - * {@link #BUFFERING_MODE_TIME_ONLY}, {@link #BUFFERING_MODE_SIZE_ONLY} and - * {@link #BUFFERING_MODE_TIME_THEN_SIZE}. - * {@link MediaPlayer} source component has default buffering modes which can be queried - * by calling {@link MediaPlayer#getDefaultBufferingParams()}. - * Users should always use those default modes or their downsized version when trying to - * change buffering params. For example, {@link #BUFFERING_MODE_TIME_THEN_SIZE} can be - * downsized to {@link #BUFFERING_MODE_NONE}, {@link #BUFFERING_MODE_TIME_ONLY} or - * {@link #BUFFERING_MODE_SIZE_ONLY}. But {@link #BUFFERING_MODE_TIME_ONLY} can not be - * downsized to {@link #BUFFERING_MODE_SIZE_ONLY}. + * <p>{@link MediaPlayer} source component has default marks which can be queried by + * calling {@link MediaPlayer#getBufferingParams()} before any change is made by + * {@link MediaPlayer#setBufferingParams()}. * <ul> - * <li><strong>initial buffering stage:</strong> has one watermark which is used when - * {@link MediaPlayer} is being prepared. When cached data amount exceeds this watermark, - * {@link MediaPlayer} is prepared.</li> - * <li><strong>rebuffering stage:</strong> has two watermarks, low and high, which are - * used when {@link MediaPlayer} is playing back content. + * <li><strong>initial buffering:</strong> initialMarkMs is used when + * {@link MediaPlayer} is being prepared. When cached data amount exceeds this mark + * {@link MediaPlayer} is prepared. </li> + * <li><strong>rebuffering during playback:</strong> resumePlaybackMarkMs is used when + * {@link MediaPlayer} is playing back content. * <ul> - * <li> When cached data amount exceeds high watermark, {@link MediaPlayer} will pause - * buffering. Buffering will resume when cache runs below some limit which could be low - * watermark or some intermediate value decided by the source component.</li> - * <li> When cached data amount runs below low watermark, {@link MediaPlayer} will paused - * playback. Playback will resume when cached data amount exceeds high watermark - * or reaches end of stream.</li> - * </ul> + * <li> {@link MediaPlayer} has internal mark, namely pausePlaybackMarkMs, to decide when + * to pause playback if cached data amount runs low. This internal mark varies based on + * type of data source. </li> + * <li> When cached data amount exceeds resumePlaybackMarkMs, {@link MediaPlayer} will + * resume playback if it has been paused due to low cached data amount. The internal mark + * pausePlaybackMarkMs shall be less than resumePlaybackMarkMs. </li> + * <li> {@link MediaPlayer} has internal mark, namely pauseRebufferingMarkMs, to decide + * when to pause rebuffering. Apparently, this internal mark shall be no less than + * resumePlaybackMarkMs. </li> + * <li> {@link MediaPlayer} has internal mark, namely resumeRebufferingMarkMs, to decide + * when to resume buffering. This internal mark varies based on type of data source. This + * mark shall be larger than pausePlaybackMarkMs, and less than pauseRebufferingMarkMs. + * </li> + * </ul> </li> * </ul> * <p>Users should use {@link Builder} to change {@link BufferingParams}. * @hide */ public final class BufferingParams implements Parcelable { - /** - * This mode indicates that source buffering is not supported. - */ - public static final int BUFFERING_MODE_NONE = 0; - /** - * This mode indicates that only time based source buffering is supported. This means - * the watermark(s) are time based. - */ - public static final int BUFFERING_MODE_TIME_ONLY = 1; - /** - * This mode indicates that only size based source buffering is supported. This means - * the watermark(s) are size based. - */ - public static final int BUFFERING_MODE_SIZE_ONLY = 2; - /** - * This mode indicates that both time and size based source buffering are supported, - * and time based calculation precedes size based. Size based calculation will be used - * only when time information is not available from the source. - */ - public static final int BUFFERING_MODE_TIME_THEN_SIZE = 3; - - /** @hide */ - @IntDef( - value = { - BUFFERING_MODE_NONE, - BUFFERING_MODE_TIME_ONLY, - BUFFERING_MODE_SIZE_ONLY, - BUFFERING_MODE_TIME_THEN_SIZE, - } - ) - @Retention(RetentionPolicy.SOURCE) - public @interface BufferingMode {} - - private static final int BUFFERING_NO_WATERMARK = -1; + private static final int BUFFERING_NO_MARK = -1; // params - private int mInitialBufferingMode = BUFFERING_MODE_NONE; - private int mRebufferingMode = BUFFERING_MODE_NONE; - - private int mInitialWatermarkMs = BUFFERING_NO_WATERMARK; - private int mInitialWatermarkKB = BUFFERING_NO_WATERMARK; + private int mInitialMarkMs = BUFFERING_NO_MARK; - private int mRebufferingWatermarkLowMs = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkHighMs = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkLowKB = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkHighKB = BUFFERING_NO_WATERMARK; + private int mResumePlaybackMarkMs = BUFFERING_NO_MARK; private BufferingParams() { } /** - * Return the initial buffering mode used when {@link MediaPlayer} is being prepared. - * @return one of the values that can be set in {@link Builder#setInitialBufferingMode(int)} - */ - public int getInitialBufferingMode() { - return mInitialBufferingMode; - } - - /** - * Return the rebuffering mode used when {@link MediaPlayer} is playing back source. - * @return one of the values that can be set in {@link Builder#setRebufferingMode(int)} - */ - public int getRebufferingMode() { - return mRebufferingMode; - } - - /** - * Return the time based initial buffering watermark in milliseconds. - * It is meaningful only when initial buffering mode obatined from - * {@link #getInitialBufferingMode()} is time based. - * @return time based initial buffering watermark in milliseconds - */ - public int getInitialBufferingWatermarkMs() { - return mInitialWatermarkMs; - } - - /** - * Return the size based initial buffering watermark in kilobytes. - * It is meaningful only when initial buffering mode obatined from - * {@link #getInitialBufferingMode()} is size based. - * @return size based initial buffering watermark in kilobytes + * Return initial buffering mark in milliseconds. + * @return initial buffering mark in milliseconds */ - public int getInitialBufferingWatermarkKB() { - return mInitialWatermarkKB; + public int getInitialMarkMs() { + return mInitialMarkMs; } /** - * Return the time based low watermark in milliseconds for rebuffering. - * It is meaningful only when rebuffering mode obatined from - * {@link #getRebufferingMode()} is time based. - * @return time based low watermark for rebuffering in milliseconds + * Return the mark in milliseconds for resuming playback. + * @return the mark for resuming playback in milliseconds */ - public int getRebufferingWatermarkLowMs() { - return mRebufferingWatermarkLowMs; - } - - /** - * Return the time based high watermark in milliseconds for rebuffering. - * It is meaningful only when rebuffering mode obatined from - * {@link #getRebufferingMode()} is time based. - * @return time based high watermark for rebuffering in milliseconds - */ - public int getRebufferingWatermarkHighMs() { - return mRebufferingWatermarkHighMs; - } - - /** - * Return the size based low watermark in kilobytes for rebuffering. - * It is meaningful only when rebuffering mode obatined from - * {@link #getRebufferingMode()} is size based. - * @return size based low watermark for rebuffering in kilobytes - */ - public int getRebufferingWatermarkLowKB() { - return mRebufferingWatermarkLowKB; - } - - /** - * Return the size based high watermark in kilobytes for rebuffering. - * It is meaningful only when rebuffering mode obatined from - * {@link #getRebufferingMode()} is size based. - * @return size based high watermark for rebuffering in kilobytes - */ - public int getRebufferingWatermarkHighKB() { - return mRebufferingWatermarkHighKB; + public int getResumePlaybackMarkMs() { + return mResumePlaybackMarkMs; } /** @@ -200,27 +98,19 @@ public final class BufferingParams implements Parcelable { * <pre class="prettyprint"> * BufferingParams myParams = mediaplayer.getDefaultBufferingParams(); * myParams = new BufferingParams.Builder(myParams) - * .setInitialBufferingWatermarkMs(10000) - * .build(); + * .setInitialMarkMs(10000) + * .setResumePlaybackMarkMs(15000) + * .build(); * mediaplayer.setBufferingParams(myParams); * </pre> */ public static class Builder { - private int mInitialBufferingMode = BUFFERING_MODE_NONE; - private int mRebufferingMode = BUFFERING_MODE_NONE; - - private int mInitialWatermarkMs = BUFFERING_NO_WATERMARK; - private int mInitialWatermarkKB = BUFFERING_NO_WATERMARK; - - private int mRebufferingWatermarkLowMs = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkHighMs = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkLowKB = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkHighKB = BUFFERING_NO_WATERMARK; + private int mInitialMarkMs = BUFFERING_NO_MARK; + private int mResumePlaybackMarkMs = BUFFERING_NO_MARK; /** * Constructs a new Builder with the defaults. - * By default, both initial buffering mode and rebuffering mode are - * {@link BufferingParams#BUFFERING_MODE_NONE}, and all watermarks are -1. + * By default, all marks are -1. */ public Builder() { } @@ -231,16 +121,8 @@ public final class BufferingParams implements Parcelable { * in the new Builder. */ public Builder(BufferingParams bp) { - mInitialBufferingMode = bp.mInitialBufferingMode; - mRebufferingMode = bp.mRebufferingMode; - - mInitialWatermarkMs = bp.mInitialWatermarkMs; - mInitialWatermarkKB = bp.mInitialWatermarkKB; - - mRebufferingWatermarkLowMs = bp.mRebufferingWatermarkLowMs; - mRebufferingWatermarkHighMs = bp.mRebufferingWatermarkHighMs; - mRebufferingWatermarkLowKB = bp.mRebufferingWatermarkLowKB; - mRebufferingWatermarkHighKB = bp.mRebufferingWatermarkHighKB; + mInitialMarkMs = bp.mInitialMarkMs; + mResumePlaybackMarkMs = bp.mResumePlaybackMarkMs; } /** @@ -250,179 +132,37 @@ public final class BufferingParams implements Parcelable { * @return a new {@link BufferingParams} object */ public BufferingParams build() { - if (isTimeBasedMode(mRebufferingMode) - && mRebufferingWatermarkLowMs > mRebufferingWatermarkHighMs) { - throw new IllegalStateException("Illegal watermark:" - + mRebufferingWatermarkLowMs + " : " + mRebufferingWatermarkHighMs); - } - if (isSizeBasedMode(mRebufferingMode) - && mRebufferingWatermarkLowKB > mRebufferingWatermarkHighKB) { - throw new IllegalStateException("Illegal watermark:" - + mRebufferingWatermarkLowKB + " : " + mRebufferingWatermarkHighKB); - } - BufferingParams bp = new BufferingParams(); - bp.mInitialBufferingMode = mInitialBufferingMode; - bp.mRebufferingMode = mRebufferingMode; - - bp.mInitialWatermarkMs = mInitialWatermarkMs; - bp.mInitialWatermarkKB = mInitialWatermarkKB; + bp.mInitialMarkMs = mInitialMarkMs; + bp.mResumePlaybackMarkMs = mResumePlaybackMarkMs; - bp.mRebufferingWatermarkLowMs = mRebufferingWatermarkLowMs; - bp.mRebufferingWatermarkHighMs = mRebufferingWatermarkHighMs; - bp.mRebufferingWatermarkLowKB = mRebufferingWatermarkLowKB; - bp.mRebufferingWatermarkHighKB = mRebufferingWatermarkHighKB; return bp; } - private boolean isTimeBasedMode(int mode) { - return (mode == BUFFERING_MODE_TIME_ONLY || mode == BUFFERING_MODE_TIME_THEN_SIZE); - } - - private boolean isSizeBasedMode(int mode) { - return (mode == BUFFERING_MODE_SIZE_ONLY || mode == BUFFERING_MODE_TIME_THEN_SIZE); - } - /** - * Sets the initial buffering mode. - * @param mode one of {@link BufferingParams#BUFFERING_MODE_NONE}, - * {@link BufferingParams#BUFFERING_MODE_TIME_ONLY}, - * {@link BufferingParams#BUFFERING_MODE_SIZE_ONLY}, - * {@link BufferingParams#BUFFERING_MODE_TIME_THEN_SIZE}, + * Sets the time based mark in milliseconds for initial buffering. + * @param markMs time based mark in milliseconds * @return the same Builder instance. */ - public Builder setInitialBufferingMode(@BufferingMode int mode) { - switch (mode) { - case BUFFERING_MODE_NONE: - case BUFFERING_MODE_TIME_ONLY: - case BUFFERING_MODE_SIZE_ONLY: - case BUFFERING_MODE_TIME_THEN_SIZE: - mInitialBufferingMode = mode; - break; - default: - throw new IllegalArgumentException("Illegal buffering mode " + mode); - } + public Builder setInitialMarkMs(int markMs) { + mInitialMarkMs = markMs; return this; } /** - * Sets the rebuffering mode. - * @param mode one of {@link BufferingParams#BUFFERING_MODE_NONE}, - * {@link BufferingParams#BUFFERING_MODE_TIME_ONLY}, - * {@link BufferingParams#BUFFERING_MODE_SIZE_ONLY}, - * {@link BufferingParams#BUFFERING_MODE_TIME_THEN_SIZE}, + * Sets the time based mark in milliseconds for resuming playback. + * @param markMs time based mark in milliseconds for resuming playback * @return the same Builder instance. */ - public Builder setRebufferingMode(@BufferingMode int mode) { - switch (mode) { - case BUFFERING_MODE_NONE: - case BUFFERING_MODE_TIME_ONLY: - case BUFFERING_MODE_SIZE_ONLY: - case BUFFERING_MODE_TIME_THEN_SIZE: - mRebufferingMode = mode; - break; - default: - throw new IllegalArgumentException("Illegal buffering mode " + mode); - } - return this; - } - - /** - * Sets the time based watermark in milliseconds for initial buffering. - * @param watermarkMs time based watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setInitialBufferingWatermarkMs(int watermarkMs) { - mInitialWatermarkMs = watermarkMs; - return this; - } - - /** - * Sets the size based watermark in kilobytes for initial buffering. - * @param watermarkKB size based watermark in kilobytes - * @return the same Builder instance. - */ - public Builder setInitialBufferingWatermarkKB(int watermarkKB) { - mInitialWatermarkKB = watermarkKB; - return this; - } - - /** - * Sets the time based low watermark in milliseconds for rebuffering. - * @param watermarkMs time based low watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarkLowMs(int watermarkMs) { - mRebufferingWatermarkLowMs = watermarkMs; - return this; - } - - /** - * Sets the time based high watermark in milliseconds for rebuffering. - * @param watermarkMs time based high watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarkHighMs(int watermarkMs) { - mRebufferingWatermarkHighMs = watermarkMs; - return this; - } - - /** - * Sets the size based low watermark in milliseconds for rebuffering. - * @param watermarkKB size based low watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarkLowKB(int watermarkKB) { - mRebufferingWatermarkLowKB = watermarkKB; - return this; - } - - /** - * Sets the size based high watermark in milliseconds for rebuffering. - * @param watermarkKB size based high watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarkHighKB(int watermarkKB) { - mRebufferingWatermarkHighKB = watermarkKB; - return this; - } - - /** - * Sets the time based low and high watermarks in milliseconds for rebuffering. - * @param lowWatermarkMs time based low watermark in milliseconds - * @param highWatermarkMs time based high watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarksMs(int lowWatermarkMs, int highWatermarkMs) { - mRebufferingWatermarkLowMs = lowWatermarkMs; - mRebufferingWatermarkHighMs = highWatermarkMs; - return this; - } - - /** - * Sets the size based low and high watermarks in kilobytes for rebuffering. - * @param lowWatermarkKB size based low watermark in kilobytes - * @param highWatermarkKB size based high watermark in kilobytes - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarksKB(int lowWatermarkKB, int highWatermarkKB) { - mRebufferingWatermarkLowKB = lowWatermarkKB; - mRebufferingWatermarkHighKB = highWatermarkKB; + public Builder setResumePlaybackMarkMs(int markMs) { + mResumePlaybackMarkMs = markMs; return this; } } private BufferingParams(Parcel in) { - mInitialBufferingMode = in.readInt(); - mRebufferingMode = in.readInt(); - - mInitialWatermarkMs = in.readInt(); - mInitialWatermarkKB = in.readInt(); - - mRebufferingWatermarkLowMs = in.readInt(); - mRebufferingWatermarkHighMs = in.readInt(); - mRebufferingWatermarkLowKB = in.readInt(); - mRebufferingWatermarkHighKB = in.readInt(); + mInitialMarkMs = in.readInt(); + mResumePlaybackMarkMs = in.readInt(); } public static final Parcelable.Creator<BufferingParams> CREATOR = @@ -446,15 +186,7 @@ public final class BufferingParams implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mInitialBufferingMode); - dest.writeInt(mRebufferingMode); - - dest.writeInt(mInitialWatermarkMs); - dest.writeInt(mInitialWatermarkKB); - - dest.writeInt(mRebufferingWatermarkLowMs); - dest.writeInt(mRebufferingWatermarkHighMs); - dest.writeInt(mRebufferingWatermarkLowKB); - dest.writeInt(mRebufferingWatermarkHighKB); + dest.writeInt(mInitialMarkMs); + dest.writeInt(mResumePlaybackMarkMs); } } diff --git a/media/java/android/media/DataSourceDesc.java b/media/java/android/media/DataSourceDesc.java new file mode 100644 index 000000000000..73fad7ad4bf3 --- /dev/null +++ b/media/java/android/media/DataSourceDesc.java @@ -0,0 +1,465 @@ +/* + * Copyright 2018 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.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.io.FileDescriptor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.HttpCookie; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Structure for data source descriptor. + * + * Used by {@link MediaPlayer2#setDataSource(DataSourceDesc)} + * to set data source for playback. + * + * <p>Users should use {@link Builder} to change {@link DataSourceDesc}. + * + */ +public final class DataSourceDesc { + /* No data source has been set yet */ + public static final int TYPE_NONE = 0; + /* data source is type of MediaDataSource */ + public static final int TYPE_CALLBACK = 1; + /* data source is type of FileDescriptor */ + public static final int TYPE_FD = 2; + /* data source is type of Uri */ + public static final int TYPE_URI = 3; + + // intentionally less than long.MAX_VALUE + public static final long LONG_MAX = 0x7ffffffffffffffL; + + private int mType = TYPE_NONE; + + private Media2DataSource mMedia2DataSource; + + private FileDescriptor mFD; + private long mFDOffset = 0; + private long mFDLength = LONG_MAX; + + private Uri mUri; + private Map<String, String> mUriHeader; + private List<HttpCookie> mUriCookies; + private Context mUriContext; + + private long mId = 0; + private long mStartPositionMs = 0; + private long mEndPositionMs = LONG_MAX; + + private DataSourceDesc() { + } + + /** + * Return the Id of data source. + * @return the Id of data source + */ + public long getId() { + return mId; + } + + /** + * Return the position in milliseconds at which the playback will start. + * @return the position in milliseconds at which the playback will start + */ + public long getStartPosition() { + return mStartPositionMs; + } + + /** + * Return the position in milliseconds at which the playback will end. + * -1 means ending at the end of source content. + * @return the position in milliseconds at which the playback will end + */ + public long getEndPosition() { + return mEndPositionMs; + } + + /** + * Return the type of data source. + * @return the type of data source + */ + public int getType() { + return mType; + } + + /** + * Return the Media2DataSource of this data source. + * It's meaningful only when {@code getType} returns {@link #TYPE_CALLBACK}. + * @return the Media2DataSource of this data source + */ + public Media2DataSource getMedia2DataSource() { + return mMedia2DataSource; + } + + /** + * Return the FileDescriptor of this data source. + * It's meaningful only when {@code getType} returns {@link #TYPE_FD}. + * @return the FileDescriptor of this data source + */ + public FileDescriptor getFileDescriptor() { + return mFD; + } + + /** + * Return the offset associated with the FileDescriptor of this data source. + * It's meaningful only when {@code getType} returns {@link #TYPE_FD} and it has + * been set by the {@link Builder}. + * @return the offset associated with the FileDescriptor of this data source + */ + public long getFileDescriptorOffset() { + return mFDOffset; + } + + /** + * Return the content length associated with the FileDescriptor of this data source. + * It's meaningful only when {@code getType} returns {@link #TYPE_FD}. + * -1 means same as the length of source content. + * @return the content length associated with the FileDescriptor of this data source + */ + public long getFileDescriptorLength() { + return mFDLength; + } + + /** + * Return the Uri of this data source. + * It's meaningful only when {@code getType} returns {@link #TYPE_URI}. + * @return the Uri of this data source + */ + public Uri getUri() { + return mUri; + } + + /** + * Return the Uri headers of this data source. + * It's meaningful only when {@code getType} returns {@link #TYPE_URI}. + * @return the Uri headers of this data source + */ + public Map<String, String> getUriHeaders() { + if (mUriHeader == null) { + return null; + } + return new HashMap<String, String>(mUriHeader); + } + + /** + * Return the Uri cookies of this data source. + * It's meaningful only when {@code getType} returns {@link #TYPE_URI}. + * @return the Uri cookies of this data source + */ + public List<HttpCookie> getUriCookies() { + if (mUriCookies == null) { + return null; + } + return new ArrayList<HttpCookie>(mUriCookies); + } + + /** + * Return the Context used for resolving the Uri of this data source. + * It's meaningful only when {@code getType} returns {@link #TYPE_URI}. + * @return the Context used for resolving the Uri of this data source + */ + public Context getUriContext() { + return mUriContext; + } + + /** + * Builder class for {@link DataSourceDesc} objects. + * <p> Here is an example where <code>Builder</code> is used to define the + * {@link DataSourceDesc} to be used by a {@link MediaPlayer2} instance: + * + * <pre class="prettyprint"> + * DataSourceDesc oldDSD = mediaplayer2.getDataSourceDesc(); + * DataSourceDesc newDSD = new DataSourceDesc.Builder(oldDSD) + * .setStartPosition(1000) + * .setEndPosition(15000) + * .build(); + * mediaplayer2.setDataSourceDesc(newDSD); + * </pre> + */ + public static class Builder { + private int mType = TYPE_NONE; + + private Media2DataSource mMedia2DataSource; + + private FileDescriptor mFD; + private long mFDOffset = 0; + private long mFDLength = LONG_MAX; + + private Uri mUri; + private Map<String, String> mUriHeader; + private List<HttpCookie> mUriCookies; + private Context mUriContext; + + private long mId = 0; + private long mStartPositionMs = 0; + private long mEndPositionMs = LONG_MAX; + + /** + * Constructs a new Builder with the defaults. + */ + public Builder() { + } + + /** + * Constructs a new Builder from a given {@link DataSourceDesc} instance + * @param dsd the {@link DataSourceDesc} object whose data will be reused + * in the new Builder. + */ + public Builder(DataSourceDesc dsd) { + mType = dsd.mType; + mMedia2DataSource = dsd.mMedia2DataSource; + mFD = dsd.mFD; + mFDOffset = dsd.mFDOffset; + mFDLength = dsd.mFDLength; + mUri = dsd.mUri; + mUriHeader = dsd.mUriHeader; + mUriCookies = dsd.mUriCookies; + mUriContext = dsd.mUriContext; + + mId = dsd.mId; + mStartPositionMs = dsd.mStartPositionMs; + mEndPositionMs = dsd.mEndPositionMs; + } + + /** + * Combines all of the fields that have been set and return a new + * {@link DataSourceDesc} object. <code>IllegalStateException</code> will be + * thrown if there is conflict between fields. + * + * @return a new {@link DataSourceDesc} object + */ + public DataSourceDesc build() { + if (mType != TYPE_CALLBACK + && mType != TYPE_FD + && mType != TYPE_URI) { + throw new IllegalStateException("Illegal type: " + mType); + } + if (mStartPositionMs > mEndPositionMs) { + throw new IllegalStateException("Illegal start/end position: " + + mStartPositionMs + " : " + mEndPositionMs); + } + + DataSourceDesc dsd = new DataSourceDesc(); + dsd.mType = mType; + dsd.mMedia2DataSource = mMedia2DataSource; + dsd.mFD = mFD; + dsd.mFDOffset = mFDOffset; + dsd.mFDLength = mFDLength; + dsd.mUri = mUri; + dsd.mUriHeader = mUriHeader; + dsd.mUriCookies = mUriCookies; + dsd.mUriContext = mUriContext; + + dsd.mId = mId; + dsd.mStartPositionMs = mStartPositionMs; + dsd.mEndPositionMs = mEndPositionMs; + + return dsd; + } + + /** + * Sets the Id of this data source. + * + * @param id the Id of this data source + * @return the same Builder instance. + */ + public Builder setId(long id) { + mId = id; + return this; + } + + /** + * Sets the start position in milliseconds at which the playback will start. + * Any negative number is treated as 0. + * + * @param position the start position in milliseconds at which the playback will start + * @return the same Builder instance. + * + */ + public Builder setStartPosition(long position) { + if (position < 0) { + position = 0; + } + mStartPositionMs = position; + return this; + } + + /** + * Sets the end position in milliseconds at which the playback will end. + * Any negative number is treated as maximum length of the data source. + * + * @param position the end position in milliseconds at which the playback will end + * @return the same Builder instance. + */ + public Builder setEndPosition(long position) { + if (position < 0) { + position = LONG_MAX; + } + mEndPositionMs = position; + return this; + } + + /** + * Sets the data source (Media2DataSource) to use. + * + * @param m2ds the Media2DataSource for the media you want to play + * @return the same Builder instance. + * @throws NullPointerException if m2ds is null. + */ + public Builder setDataSource(Media2DataSource m2ds) { + Preconditions.checkNotNull(m2ds); + resetDataSource(); + mType = TYPE_CALLBACK; + mMedia2DataSource = m2ds; + return this; + } + + /** + * Sets the data source (FileDescriptor) to use. The FileDescriptor must be + * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility + * to close the file descriptor after the source has been used. + * + * @param fd the FileDescriptor for the file you want to play + * @return the same Builder instance. + * @throws NullPointerException if fd is null. + */ + public Builder setDataSource(FileDescriptor fd) { + Preconditions.checkNotNull(fd); + resetDataSource(); + mType = TYPE_FD; + mFD = fd; + return this; + } + + /** + * Sets the data source (FileDescriptor) to use. The FileDescriptor must be + * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility + * to close the file descriptor after the source has been used. + * + * Any negative number for offset is treated as 0. + * Any negative number for length is treated as maximum length of the data source. + * + * @param fd the FileDescriptor for the file you want to play + * @param offset the offset into the file where the data to be played starts, in bytes + * @param length the length in bytes of the data to be played + * @return the same Builder instance. + * @throws NullPointerException if fd is null. + */ + public Builder setDataSource(FileDescriptor fd, long offset, long length) { + Preconditions.checkNotNull(fd); + if (offset < 0) { + offset = 0; + } + if (length < 0) { + length = LONG_MAX; + } + resetDataSource(); + mType = TYPE_FD; + mFD = fd; + mFDOffset = offset; + mFDLength = length; + return this; + } + + /** + * Sets the data source as a content Uri. + * + * @param context the Context to use when resolving the Uri + * @param uri the Content URI of the data you want to play + * @return the same Builder instance. + * @throws NullPointerException if context or uri is null. + */ + public Builder setDataSource(@NonNull Context context, @NonNull Uri uri) { + Preconditions.checkNotNull(context, "context cannot be null"); + Preconditions.checkNotNull(uri, "uri cannot be null"); + resetDataSource(); + mType = TYPE_URI; + mUri = uri; + mUriContext = context; + return this; + } + + /** + * Sets the data source as a content Uri. + * + * To provide cookies for the subsequent HTTP requests, you can install your own default + * cookie handler and use other variants of setDataSource APIs instead. Alternatively, you + * can use this API to pass the cookies as a list of HttpCookie. If the app has not + * installed a CookieHandler already, {@link MediaPlayer2} will create a CookieManager + * and populates its CookieStore with the provided cookies when this data source is passed + * to {@link MediaPlayer2}. If the app has installed its own handler already, the handler + * is required to be of CookieManager type such that {@link MediaPlayer2} can update the + * manager’s CookieStore. + * + * <p><strong>Note</strong> that the cross domain redirection is allowed by default, + * but that can be changed with key/value pairs through the headers parameter with + * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to + * disallow or allow cross domain redirection. + * + * @param context the Context to use when resolving the Uri + * @param uri the Content URI of the data you want to play + * @param headers the headers to be sent together with the request for the data + * The headers must not include cookies. Instead, use the cookies param. + * @param cookies the cookies to be sent together with the request + * @return the same Builder instance. + * @throws NullPointerException if context or uri is null. + */ + public Builder setDataSource(@NonNull Context context, @NonNull Uri uri, + @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) { + Preconditions.checkNotNull(uri); + resetDataSource(); + mType = TYPE_URI; + mUri = uri; + if (headers != null) { + mUriHeader = new HashMap<String, String>(headers); + } + if (cookies != null) { + mUriCookies = new ArrayList<HttpCookie>(cookies); + } + mUriContext = context; + return this; + } + + private void resetDataSource() { + mType = TYPE_NONE; + mMedia2DataSource = null; + mFD = null; + mFDOffset = 0; + mFDLength = LONG_MAX; + mUri = null; + mUriHeader = null; + mUriCookies = null; + mUriContext = null; + } + } +} diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index bf3a3b9b07bb..91754162180f 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -2078,7 +2078,8 @@ public class ExifInterface { } } - private static float convertRationalLatLonToFloat(String rationalString, String ref) { + /** {@hide} */ + public static float convertRationalLatLonToFloat(String rationalString, String ref) { try { String [] parts = rationalString.split(","); @@ -2563,51 +2564,66 @@ public class ExifInterface { }); } + String hasImage = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE); String hasVideo = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); - final String METADATA_HAS_VIDEO_VALUE_YES = "yes"; - if (METADATA_HAS_VIDEO_VALUE_YES.equals(hasVideo)) { - String width = retriever.extractMetadata( + String width = null; + String height = null; + String rotation = null; + final String METADATA_VALUE_YES = "yes"; + // If the file has both image and video, prefer image info over video info. + // App querying ExifInterface is most likely using the bitmap path which + // picks the image first. + if (METADATA_VALUE_YES.equals(hasImage)) { + width = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH); + height = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT); + rotation = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION); + } else if (METADATA_VALUE_YES.equals(hasVideo)) { + width = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); - String height = retriever.extractMetadata( + height = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + rotation = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + } - if (width != null) { - mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, - ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder)); - } - - if (height != null) { - mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, - ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder)); - } + if (width != null) { + mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, + ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder)); + } - String rotation = retriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); - if (rotation != null) { - int orientation = ExifInterface.ORIENTATION_NORMAL; + if (height != null) { + mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, + ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder)); + } - // all rotation angles in CW - switch (Integer.parseInt(rotation)) { - case 90: - orientation = ExifInterface.ORIENTATION_ROTATE_90; - break; - case 180: - orientation = ExifInterface.ORIENTATION_ROTATE_180; - break; - case 270: - orientation = ExifInterface.ORIENTATION_ROTATE_270; - break; - } + if (rotation != null) { + int orientation = ExifInterface.ORIENTATION_NORMAL; - mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION, - ExifAttribute.createUShort(orientation, mExifByteOrder)); + // all rotation angles in CW + switch (Integer.parseInt(rotation)) { + case 90: + orientation = ExifInterface.ORIENTATION_ROTATE_90; + break; + case 180: + orientation = ExifInterface.ORIENTATION_ROTATE_180; + break; + case 270: + orientation = ExifInterface.ORIENTATION_ROTATE_270; + break; } - if (DEBUG) { - Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation); - } + mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION, + ExifAttribute.createUShort(orientation, mExifByteOrder)); + } + + if (DEBUG) { + Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation); } } finally { retriever.release(); diff --git a/media/java/android/media/IAudioFocusDispatcher.aidl b/media/java/android/media/IAudioFocusDispatcher.aidl index 09575f733e32..3b33c5b7a46a 100644 --- a/media/java/android/media/IAudioFocusDispatcher.aidl +++ b/media/java/android/media/IAudioFocusDispatcher.aidl @@ -25,4 +25,6 @@ oneway interface IAudioFocusDispatcher { void dispatchAudioFocusChange(int focusChange, String clientId); + void dispatchFocusResultFromExtPolicy(int requestResult, String clientId); + } diff --git a/media/java/android/media/IAudioServerStateDispatcher.aidl b/media/java/android/media/IAudioServerStateDispatcher.aidl new file mode 100644 index 000000000000..2bc90eaf1780 --- /dev/null +++ b/media/java/android/media/IAudioServerStateDispatcher.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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; + +/** + * AIDL for the AudioService to signal audio server state changes + * + * {@hide} + */ +oneway interface IAudioServerStateDispatcher { + + void dispatchAudioServerStateChange(boolean state); + +} diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 6c6522328e8d..05ba4c35c21d 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -16,9 +16,7 @@ package android.media; -import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; -import android.content.ComponentName; import android.media.AudioAttributes; import android.media.AudioFocusInfo; import android.media.AudioPlaybackConfiguration; @@ -26,30 +24,40 @@ import android.media.AudioRecordingConfiguration; import android.media.AudioRoutesInfo; import android.media.IAudioFocusDispatcher; import android.media.IAudioRoutesObserver; +import android.media.IAudioServerStateDispatcher; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; import android.media.IVolumeController; +import android.media.IVolumeController; import android.media.PlayerBase; -import android.media.Rating; import android.media.VolumePolicy; import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.IAudioPolicyCallback; -import android.net.Uri; -import android.view.KeyEvent; /** * {@hide} */ interface IAudioService { + // C++ and Java methods below. - // WARNING: When methods are inserted or deleted, the transaction IDs in + // WARNING: When methods are inserted or deleted in this section, the transaction IDs in // frameworks/native/include/audiomanager/IAudioManager.h must be updated to match the order // in this file. // // When a method's argument list is changed, BpAudioManager's corresponding serialization code // (if any) in frameworks/native/services/audiomanager/IAudioManager.cpp must be updated. + int trackPlayer(in PlayerBase.PlayerIdCard pic); + + oneway void playerAttributes(in int piid, in AudioAttributes attr); + + oneway void playerEvent(in int piid, in int event); + + oneway void releasePlayer(in int piid); + + // Java-only methods below. + oneway void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags, String callingPackage, String caller); @@ -166,7 +174,8 @@ interface IAudioService { boolean isHdmiSystemAudioSupported(); String registerAudioPolicy(in AudioPolicyConfig policyConfig, - in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy); + in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy, + boolean isVolumeController); oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb); @@ -186,14 +195,6 @@ interface IAudioService { List<AudioPlaybackConfiguration> getActivePlaybackConfigurations(); - int trackPlayer(in PlayerBase.PlayerIdCard pic); - - oneway void playerAttributes(in int piid, in AudioAttributes attr); - - oneway void playerEvent(in int piid, in int event); - - oneway void releasePlayer(in int piid); - void disableRingtoneSync(in int userId); int getFocusRampTimeMs(in int focusGain, in AudioAttributes attr); @@ -206,5 +207,15 @@ interface IAudioService { int setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(in BluetoothDevice device, int state, int profile, boolean suppressNoisyIntent); - // WARNING: read warning at top of file, it is recommended to add new methods at the end + oneway void setFocusRequestResultFromExtPolicy(in AudioFocusInfo afi, int requestResult, + in IAudioPolicyCallback pcb); + + void registerAudioServerStateDispatcher(IAudioServerStateDispatcher asd); + + oneway void unregisterAudioServerStateDispatcher(IAudioServerStateDispatcher asd); + + boolean isAudioServerRunning(); + + // WARNING: read warning at top of file, new methods that need to be used by native + // code via IAudioManager.h need to be added to the top section. } diff --git a/media/java/android/media/IPlayer.aidl b/media/java/android/media/ISessionTokensListener.aidl index 2d60bf956904..c83a19e64d66 100644 --- a/media/java/android/media/IPlayer.aidl +++ b/media/java/android/media/ISessionTokensListener.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright 2018 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. @@ -16,18 +16,12 @@ package android.media; -import android.media.VolumeShaper; +import android.os.Bundle; /** + * Listens for changes to the list of session tokens. * @hide */ -interface IPlayer { - oneway void start(); - oneway void pause(); - oneway void stop(); - oneway void setVolume(float vol); - oneway void setPan(float pan); - oneway void setStartDelayMs(int delayMs); - oneway void applyVolumeShaper(in VolumeShaper.Configuration configuration, - in VolumeShaper.Operation operation); +oneway interface ISessionTokensListener { + void onSessionTokensChanged(in List<Bundle> tokens); } diff --git a/media/java/android/media/ImageReader.java b/media/java/android/media/ImageReader.java index c78c99f7a228..1019580589ab 100644 --- a/media/java/android/media/ImageReader.java +++ b/media/java/android/media/ImageReader.java @@ -640,7 +640,6 @@ public class ImageReader implements AutoCloseable { * The ImageReader continues to be usable after this call, but may need to reallocate buffers * when more buffers are needed for rendering. * </p> - * @hide */ public void discardFreeBuffers() { synchronized (mCloseLock) { diff --git a/media/java/android/media/Media2DataSource.java b/media/java/android/media/Media2DataSource.java new file mode 100644 index 000000000000..8ee4a705b446 --- /dev/null +++ b/media/java/android/media/Media2DataSource.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017 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 java.io.Closeable; +import java.io.IOException; + +/** + * For supplying media data to the framework. Implement this if your app has + * special requirements for the way media data is obtained. + * + * <p class="note">Methods of this interface may be called on multiple different + * threads. There will be a thread synchronization point between each call to ensure that + * modifications to the state of your Media2DataSource are visible to future calls. This means + * you don't need to do your own synchronization unless you're modifying the + * Media2DataSource from another thread while it's being used by the framework.</p> + * + */ +public abstract class Media2DataSource implements Closeable { + /** + * Called to request data from the given position. + * + * Implementations should should write up to {@code size} bytes into + * {@code buffer}, and return the number of bytes written. + * + * Return {@code 0} if size is zero (thus no bytes are read). + * + * Return {@code -1} to indicate that end of stream is reached. + * + * @param position the position in the data source to read from. + * @param buffer the buffer to read the data into. + * @param offset the offset within buffer to read the data into. + * @param size the number of bytes to read. + * @throws IOException on fatal errors. + * @return the number of bytes read, or -1 if there was an error. + */ + public abstract int readAt(long position, byte[] buffer, int offset, int size) + throws IOException; + + /** + * Called to get the size of the data source. + * + * @throws IOException on fatal errors + * @return the size of data source in bytes, or -1 if the size is unknown. + */ + public abstract long getSize() throws IOException; +} diff --git a/media/java/android/media/Media2HTTPConnection.java b/media/java/android/media/Media2HTTPConnection.java new file mode 100644 index 000000000000..0d7825a0853f --- /dev/null +++ b/media/java/android/media/Media2HTTPConnection.java @@ -0,0 +1,385 @@ +/* + * Copyright 2017 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.net.NetworkUtils; +import android.os.StrictMode; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.Proxy; +import java.net.URL; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.NoRouteToHostException; +import java.net.ProtocolException; +import java.net.UnknownServiceException; +import java.util.HashMap; +import java.util.Map; + +import static android.media.MediaPlayer2.MEDIA_ERROR_UNSUPPORTED; + +/** @hide */ +public class Media2HTTPConnection { + private static final String TAG = "Media2HTTPConnection"; + private static final boolean VERBOSE = false; + + // connection timeout - 30 sec + private static final int CONNECT_TIMEOUT_MS = 30 * 1000; + + private long mCurrentOffset = -1; + private URL mURL = null; + private Map<String, String> mHeaders = null; + private HttpURLConnection mConnection = null; + private long mTotalSize = -1; + private InputStream mInputStream = null; + + private boolean mAllowCrossDomainRedirect = true; + private boolean mAllowCrossProtocolRedirect = true; + + // from com.squareup.okhttp.internal.http + private final static int HTTP_TEMP_REDIRECT = 307; + private final static int MAX_REDIRECTS = 20; + + public Media2HTTPConnection() { + CookieHandler cookieHandler = CookieHandler.getDefault(); + if (cookieHandler == null) { + Log.w(TAG, "Media2HTTPConnection: Unexpected. No CookieHandler found."); + } + } + + public boolean connect(String uri, String headers) { + if (VERBOSE) { + Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers); + } + + try { + disconnect(); + mAllowCrossDomainRedirect = true; + mURL = new URL(uri); + mHeaders = convertHeaderStringToMap(headers); + } catch (MalformedURLException e) { + return false; + } + + return true; + } + + private boolean parseBoolean(String val) { + try { + return Long.parseLong(val) != 0; + } catch (NumberFormatException e) { + return "true".equalsIgnoreCase(val) || + "yes".equalsIgnoreCase(val); + } + } + + /* returns true iff header is internal */ + private boolean filterOutInternalHeaders(String key, String val) { + if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) { + mAllowCrossDomainRedirect = parseBoolean(val); + // cross-protocol redirects are also controlled by this flag + mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect; + } else { + return false; + } + return true; + } + + private Map<String, String> convertHeaderStringToMap(String headers) { + HashMap<String, String> map = new HashMap<String, String>(); + + String[] pairs = headers.split("\r\n"); + for (String pair : pairs) { + int colonPos = pair.indexOf(":"); + if (colonPos >= 0) { + String key = pair.substring(0, colonPos); + String val = pair.substring(colonPos + 1); + + if (!filterOutInternalHeaders(key, val)) { + map.put(key, val); + } + } + } + + return map; + } + + public void disconnect() { + teardownConnection(); + mHeaders = null; + mURL = null; + } + + private void teardownConnection() { + if (mConnection != null) { + if (mInputStream != null) { + try { + mInputStream.close(); + } catch (IOException e) { + } + mInputStream = null; + } + + mConnection.disconnect(); + mConnection = null; + + mCurrentOffset = -1; + } + } + + private static final boolean isLocalHost(URL url) { + if (url == null) { + return false; + } + + String host = url.getHost(); + + if (host == null) { + return false; + } + + try { + if (host.equalsIgnoreCase("localhost")) { + return true; + } + if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) { + return true; + } + } catch (IllegalArgumentException iex) { + } + return false; + } + + private void seekTo(long offset) throws IOException { + teardownConnection(); + + try { + int response; + int redirectCount = 0; + + URL url = mURL; + + // do not use any proxy for localhost (127.0.0.1) + boolean noProxy = isLocalHost(url); + + while (true) { + if (noProxy) { + mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY); + } else { + mConnection = (HttpURLConnection)url.openConnection(); + } + mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS); + + // handle redirects ourselves if we do not allow cross-domain redirect + mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect); + + if (mHeaders != null) { + for (Map.Entry<String, String> entry : mHeaders.entrySet()) { + mConnection.setRequestProperty( + entry.getKey(), entry.getValue()); + } + } + + if (offset > 0) { + mConnection.setRequestProperty( + "Range", "bytes=" + offset + "-"); + } + + response = mConnection.getResponseCode(); + if (response != HttpURLConnection.HTTP_MULT_CHOICE && + response != HttpURLConnection.HTTP_MOVED_PERM && + response != HttpURLConnection.HTTP_MOVED_TEMP && + response != HttpURLConnection.HTTP_SEE_OTHER && + response != HTTP_TEMP_REDIRECT) { + // not a redirect, or redirect handled by HttpURLConnection + break; + } + + if (++redirectCount > MAX_REDIRECTS) { + throw new NoRouteToHostException("Too many redirects: " + redirectCount); + } + + String method = mConnection.getRequestMethod(); + if (response == HTTP_TEMP_REDIRECT && + !method.equals("GET") && !method.equals("HEAD")) { + // "If the 307 status code is received in response to a + // request other than GET or HEAD, the user agent MUST NOT + // automatically redirect the request" + throw new NoRouteToHostException("Invalid redirect"); + } + String location = mConnection.getHeaderField("Location"); + if (location == null) { + throw new NoRouteToHostException("Invalid redirect"); + } + url = new URL(mURL /* TRICKY: don't use url! */, location); + if (!url.getProtocol().equals("https") && + !url.getProtocol().equals("http")) { + throw new NoRouteToHostException("Unsupported protocol redirect"); + } + boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol()); + if (!mAllowCrossProtocolRedirect && !sameProtocol) { + throw new NoRouteToHostException("Cross-protocol redirects are disallowed"); + } + boolean sameHost = mURL.getHost().equals(url.getHost()); + if (!mAllowCrossDomainRedirect && !sameHost) { + throw new NoRouteToHostException("Cross-domain redirects are disallowed"); + } + + if (response != HTTP_TEMP_REDIRECT) { + // update effective URL, unless it is a Temporary Redirect + mURL = url; + } + } + + if (mAllowCrossDomainRedirect) { + // remember the current, potentially redirected URL if redirects + // were handled by HttpURLConnection + mURL = mConnection.getURL(); + } + + if (response == HttpURLConnection.HTTP_PARTIAL) { + // Partial content, we cannot just use getContentLength + // because what we want is not just the length of the range + // returned but the size of the full content if available. + + String contentRange = + mConnection.getHeaderField("Content-Range"); + + mTotalSize = -1; + if (contentRange != null) { + // format is "bytes xxx-yyy/zzz + // where "zzz" is the total number of bytes of the + // content or '*' if unknown. + + int lastSlashPos = contentRange.lastIndexOf('/'); + if (lastSlashPos >= 0) { + String total = + contentRange.substring(lastSlashPos + 1); + + try { + mTotalSize = Long.parseLong(total); + } catch (NumberFormatException e) { + } + } + } + } else if (response != HttpURLConnection.HTTP_OK) { + throw new IOException(); + } else { + mTotalSize = mConnection.getContentLength(); + } + + if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) { + // Some servers simply ignore "Range" requests and serve + // data from the start of the content. + throw new ProtocolException(); + } + + mInputStream = + new BufferedInputStream(mConnection.getInputStream()); + + mCurrentOffset = offset; + } catch (IOException e) { + mTotalSize = -1; + teardownConnection(); + mCurrentOffset = -1; + + throw e; + } + } + + public int readAt(long offset, byte[] data, int size) { + StrictMode.ThreadPolicy policy = + new StrictMode.ThreadPolicy.Builder().permitAll().build(); + + StrictMode.setThreadPolicy(policy); + + try { + if (offset != mCurrentOffset) { + seekTo(offset); + } + + int n = mInputStream.read(data, 0, size); + + if (n == -1) { + // InputStream signals EOS using a -1 result, our semantics + // are to return a 0-length read. + n = 0; + } + + mCurrentOffset += n; + + if (VERBOSE) { + Log.d(TAG, "readAt " + offset + " / " + size + " => " + n); + } + + return n; + } catch (ProtocolException e) { + Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); + return MEDIA_ERROR_UNSUPPORTED; + } catch (NoRouteToHostException e) { + Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); + return MEDIA_ERROR_UNSUPPORTED; + } catch (UnknownServiceException e) { + Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); + return MEDIA_ERROR_UNSUPPORTED; + } catch (IOException e) { + if (VERBOSE) { + Log.d(TAG, "readAt " + offset + " / " + size + " => -1"); + } + return -1; + } catch (Exception e) { + if (VERBOSE) { + Log.d(TAG, "unknown exception " + e); + Log.d(TAG, "readAt " + offset + " / " + size + " => -1"); + } + return -1; + } + } + + public long getSize() { + if (mConnection == null) { + try { + seekTo(0); + } catch (IOException e) { + return -1; + } + } + + return mTotalSize; + } + + public String getMIMEType() { + if (mConnection == null) { + try { + seekTo(0); + } catch (IOException e) { + return "application/octet-stream"; + } + } + + return mConnection.getContentType(); + } + + public String getUri() { + return mURL.toString(); + } +} diff --git a/media/java/android/media/Media2HTTPService.java b/media/java/android/media/Media2HTTPService.java new file mode 100644 index 000000000000..957acecab13a --- /dev/null +++ b/media/java/android/media/Media2HTTPService.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2017 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.util.Log; + +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.util.List; + +/** @hide */ +public class Media2HTTPService { + private static final String TAG = "Media2HTTPService"; + private List<HttpCookie> mCookies; + private Boolean mCookieStoreInitialized = new Boolean(false); + + public Media2HTTPService(List<HttpCookie> cookies) { + mCookies = cookies; + Log.v(TAG, "Media2HTTPService(" + this + "): Cookies: " + cookies); + } + + public Media2HTTPConnection makeHTTPConnection() { + + synchronized (mCookieStoreInitialized) { + // Only need to do it once for all connections + if ( !mCookieStoreInitialized ) { + CookieHandler cookieHandler = CookieHandler.getDefault(); + if (cookieHandler == null) { + cookieHandler = new CookieManager(); + CookieHandler.setDefault(cookieHandler); + Log.v(TAG, "makeHTTPConnection: CookieManager created: " + cookieHandler); + } else { + Log.v(TAG, "makeHTTPConnection: CookieHandler (" + cookieHandler + ") exists."); + } + + // Applying the bootstrapping cookies + if ( mCookies != null ) { + if ( cookieHandler instanceof CookieManager ) { + CookieManager cookieManager = (CookieManager)cookieHandler; + CookieStore store = cookieManager.getCookieStore(); + for ( HttpCookie cookie : mCookies ) { + try { + store.add(null, cookie); + } catch ( Exception e ) { + Log.v(TAG, "makeHTTPConnection: CookieStore.add" + e); + } + //for extended debugging when needed + //Log.v(TAG, "MediaHTTPConnection adding Cookie[" + cookie.getName() + + // "]: " + cookie); + } + } else { + Log.w(TAG, "makeHTTPConnection: The installed CookieHandler is not a " + + "CookieManager. Can’t add the provided cookies to the cookie " + + "store."); + } + } // mCookies + + mCookieStoreInitialized = true; + + Log.v(TAG, "makeHTTPConnection(" + this + "): cookieHandler: " + cookieHandler + + " Cookies: " + mCookies); + } // mCookieStoreInitialized + } // synchronized + + return new Media2HTTPConnection(); + } + + /* package private */ static Media2HTTPService createHTTPService(String path) { + return createHTTPService(path, null); + } + + // when cookies are provided + static Media2HTTPService createHTTPService(String path, List<HttpCookie> cookies) { + if (path.startsWith("http://") || path.startsWith("https://")) { + return (new Media2HTTPService(cookies)); + } else if (path.startsWith("widevine://")) { + Log.d(TAG, "Widevine classic is no longer supported"); + } + + return null; + } +} diff --git a/media/java/android/media/MediaActionSound.java b/media/java/android/media/MediaActionSound.java index 983ca754acd1..dcd4dce5f3eb 100644 --- a/media/java/android/media/MediaActionSound.java +++ b/media/java/android/media/MediaActionSound.java @@ -47,11 +47,16 @@ public class MediaActionSound { private SoundPool mSoundPool; private SoundState[] mSounds; + private static final String[] SOUND_DIRS = { + "/product/media/audio/ui/", + "/system/media/audio/ui/", + }; + private static final String[] SOUND_FILES = { - "/system/media/audio/ui/camera_click.ogg", - "/system/media/audio/ui/camera_focus.ogg", - "/system/media/audio/ui/VideoRecord.ogg", - "/system/media/audio/ui/VideoStop.ogg" + "camera_click.ogg", + "camera_focus.ogg", + "VideoRecord.ogg", + "VideoStop.ogg" }; private static final String TAG = "MediaActionSound"; @@ -132,12 +137,16 @@ public class MediaActionSound { } private int loadSound(SoundState sound) { - int id = mSoundPool.load(SOUND_FILES[sound.name], 1); - if (id > 0) { - sound.state = STATE_LOADING; - sound.id = id; + final String soundFileName = SOUND_FILES[sound.name]; + for (String soundDir : SOUND_DIRS) { + int id = mSoundPool.load(soundDir + soundFileName, 1); + if (id > 0) { + sound.state = STATE_LOADING; + sound.id = id; + return id; + } } - return id; + return 0; } /** diff --git a/media/java/android/media/MediaBrowser2.java b/media/java/android/media/MediaBrowser2.java new file mode 100644 index 000000000000..32d31621aa27 --- /dev/null +++ b/media/java/android/media/MediaBrowser2.java @@ -0,0 +1,217 @@ +/* + * Copyright 2018 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.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.update.ApiLoader; +import android.media.update.MediaBrowser2Provider; +import android.os.Bundle; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Browses media content offered by a {@link MediaLibraryService2}. + * @hide + */ +public class MediaBrowser2 extends MediaController2 { + // Equals to the ((MediaBrowser2Provider) getProvider()) + private final MediaBrowser2Provider mProvider; + + /** + * Callback to listen events from {@link MediaLibraryService2}. + */ + public static class BrowserCallback extends MediaController2.ControllerCallback { + /** + * Called with the result of {@link #getLibraryRoot(Bundle)}. + * <p> + * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the library root isn't + * available. + * + * @param rootHints rootHints that you previously requested. + * @param rootMediaId media id of the library root. Can be {@code null} + * @param rootExtra extra of the library root. Can be {@code null} + */ + public void onGetRootResult(Bundle rootHints, @Nullable String rootMediaId, + @Nullable Bundle rootExtra) { } + + /** + * Called when there's change in the parent's children. + * + * @param parentId parent id that you've specified with {@link #subscribe(String, Bundle)} + * @param extras extra bundle that you've specified with {@link #subscribe(String, Bundle)} + */ + public void onChildrenChanged(@NonNull String parentId, @Nullable Bundle extras) { } + + /** + * Called when the list of items has been returned by the library service for the previous + * {@link MediaBrowser2#getChildren(String, int, int, Bundle)}. + * + * @param parentId parent id + * @param page page number that you've specified with + * {@link #getChildren(String, int, int, Bundle)} + * @param pageSize page size that you've specified with + * {@link #getChildren(String, int, int, Bundle)} + * @param extras extra bundle that you've specified with + * {@link #getChildren(String, int, int, Bundle)} + * @param result result. Can be {@code null} + */ + public void onChildrenLoaded(@NonNull String parentId, int page, int pageSize, + @Nullable Bundle extras, @Nullable List<MediaItem2> result) { } + + /** + * Called when the item has been returned by the library service for the previous + * {@link MediaBrowser2#getItem(String)} call. + * <p> + * Result can be null if there had been error. + * + * @param mediaId media id + * @param result result. Can be {@code null} + */ + public void onItemLoaded(@NonNull String mediaId, @Nullable MediaItem2 result) { } + + /** + * Called when there's change in the search result. + * + * @param query search query that you've specified with {@link #search(String, Bundle)} + * @param extras extra bundle that you've specified with {@link #search(String, Bundle)} + * @param totalItemCount The total item count for the search result + */ + public void onSearchResultChanged(@NonNull String query, @Nullable Bundle extras, + int totalItemCount) { } + + /** + * Called when the search result has been returned by the library service for the previous + * {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}. + * <p> + * Result can be null if there had been error. + * + * @param query search query that you've specified with + * {@link #getSearchResult(String, int, int, Bundle)} + * @param page page number that you've specified with + * {@link #getSearchResult(String, int, int, Bundle)} + * @param pageSize page size that you've specified with + * {@link #getSearchResult(String, int, int, Bundle)} + * @param extras extra bundle that you've specified with + * {@link #getSearchResult(String, int, int, Bundle)} + * @param result result. Can be {@code null}. + */ + public void onSearchResultLoaded(@NonNull String query, int page, int pageSize, + @Nullable Bundle extras, @Nullable List<MediaItem2> result) { } + } + + public MediaBrowser2(@NonNull Context context, @NonNull SessionToken2 token, + @NonNull @CallbackExecutor Executor executor, @NonNull BrowserCallback callback) { + super(context, token, executor, callback); + mProvider = (MediaBrowser2Provider) getProvider(); + } + + @Override + MediaBrowser2Provider createProvider(Context context, SessionToken2 token, + Executor executor, ControllerCallback callback) { + return ApiLoader.getProvider(context) + .createMediaBrowser2(context, this, token, executor, (BrowserCallback) callback); + } + + /** + * Get the library root. Result would be sent back asynchronously with the + * {@link BrowserCallback#onGetRootResult(Bundle, String, Bundle)}. + * + * @param rootHints hint for the root + * @see BrowserCallback#onGetRootResult(Bundle, String, Bundle) + */ + public void getLibraryRoot(Bundle rootHints) { + mProvider.getLibraryRoot_impl(rootHints); + } + + /** + * Subscribe to a parent id for the change in its children. When there's a change, + * {@link BrowserCallback#onChildrenChanged(String, Bundle)} will be called with the bundle + * that you've specified. You should call {@link #getChildren(String, int, int, Bundle)} to get + * the actual contents for the parent. + * + * @param parentId parent id + * @param extras extra bundle + */ + public void subscribe(String parentId, @Nullable Bundle extras) { + mProvider.subscribe_impl(parentId, extras); + } + + /** + * Unsubscribe for changes to the children of the parent, which was previously subscribed with + * {@link #subscribe(String, Bundle)}. + * + * @param parentId parent id + * @param extras extra bundle + */ + public void unsubscribe(String parentId, @Nullable Bundle extras) { + mProvider.unsubscribe_impl(parentId, extras); + } + + /** + * Get list of children under the parent. Result would be sent back asynchronously with the + * {@link BrowserCallback#onChildrenLoaded(String, int, int, Bundle, List)}. + * + * @param parentId parent id for getting the children. + * @param page page number to get the result. Starts from {@code 1} + * @param pageSize page size. Should be greater or equal to {@code 1} + * @param extras extra bundle + */ + public void getChildren(String parentId, int page, int pageSize, @Nullable Bundle extras) { + mProvider.getChildren_impl(parentId, page, pageSize, extras); + } + + /** + * Get the media item with the given media id. Result would be sent back asynchronously with the + * {@link BrowserCallback#onItemLoaded(String, MediaItem2)}. + * + * @param mediaId media id for specifying the item + */ + public void getItem(String mediaId) { + mProvider.getItem_impl(mediaId); + } + + /** + * Send a search request to the library service. When there's a change, + * {@link BrowserCallback#onSearchResultChanged(String, Bundle, int)} will be called with the + * bundle that you've specified. You should call + * {@link #getSearchResult(String, int, int, Bundle)} to get the actual search result. + * + * @param query search query. Should not be an empty string. + * @param extras extra bundle + */ + public void search(@NonNull String query, @Nullable Bundle extras) { + mProvider.search_impl(query, extras); + } + + /** + * Get the search result from lhe library service. Result would be sent back asynchronously with + * the {@link BrowserCallback#onSearchResultLoaded(String, int, int, Bundle, List)}. + * + * @param query search query that you've specified with {@link #search(String, Bundle)} + * @param page page number to get search result. Starts from {@code 1} + * @param pageSize page size. Should be greater or equal to {@code 1} + * @param extras extra bundle + */ + public void getSearchResult(@NonNull String query, int page, int pageSize, + @Nullable Bundle extras) { + mProvider.getSearchResult_impl(query, page, pageSize, extras); + } +} diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index f41e33f7c102..44d909972e37 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -2639,7 +2639,8 @@ public final class MediaCodecInfo { /** * Returns the supported range of quality values. * - * @hide + * Quality is implementation-specific. As a general rule, a higher quality + * setting results in a better image quality and a lower compression ratio. */ public Range<Integer> getQualityRange() { return mQualityRange; @@ -2751,7 +2752,7 @@ public final class MediaCodecInfo { } if (info.containsKey("feature-bitrate-modes")) { for (String mode: info.getString("feature-bitrate-modes").split(",")) { - mBitControl |= parseBitrateMode(mode); + mBitControl |= (1 << parseBitrateMode(mode)); } } diff --git a/media/java/android/media/MediaController2.java b/media/java/android/media/MediaController2.java new file mode 100644 index 000000000000..bd6c7e6bed88 --- /dev/null +++ b/media/java/android/media/MediaController2.java @@ -0,0 +1,644 @@ +/* + * Copyright 2018 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.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.content.Context; +import android.media.MediaSession2.Command; +import android.media.MediaSession2.CommandButton; +import android.media.MediaSession2.CommandGroup; +import android.media.MediaSession2.ControllerInfo; +import android.media.MediaSession2.PlaylistParams; +import android.media.session.MediaSessionManager; +import android.media.update.ApiLoader; +import android.media.update.MediaController2Provider; +import android.media.update.MediaController2Provider.PlaybackInfoProvider; +import android.net.Uri; +import android.os.Bundle; +import android.os.ResultReceiver; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Allows an app to interact with an active {@link MediaSession2} or a + * {@link MediaSessionService2} in any status. Media buttons and other commands can be sent to + * the session. + * <p> + * When you're done, use {@link #close()} to clean up resources. This also helps session service + * to be destroyed when there's no controller associated with it. + * <p> + * When controlling {@link MediaSession2}, the controller will be available immediately after + * the creation. + * <p> + * When controlling {@link MediaSessionService2}, the {@link MediaController2} would be + * available only if the session service allows this controller by + * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)} for the service. Wait + * {@link ControllerCallback#onConnected(CommandGroup)} or + * {@link ControllerCallback#onDisconnected()} for the result. + * <p> + * A controller can be created through token from {@link MediaSessionManager} if you hold the + * signature|privileged permission "android.permission.MEDIA_CONTENT_CONTROL" permission or are + * an enabled notification listener or by getting a {@link SessionToken2} directly the + * the session owner. + * <p> + * MediaController2 objects are thread-safe. + * <p> + * @see MediaSession2 + * @see MediaSessionService2 + * @hide + */ +public class MediaController2 implements AutoCloseable { + /** + * Interface for listening to change in activeness of the {@link MediaSession2}. It's + * active if and only if it has set a player. + */ + public abstract static class ControllerCallback { + /** + * Called when the controller is successfully connected to the session. The controller + * becomes available afterwards. + * + * @param allowedCommands commands that's allowed by the session. + */ + public void onConnected(CommandGroup allowedCommands) { } + + /** + * Called when the session refuses the controller or the controller is disconnected from + * the session. The controller becomes unavailable afterwards and the callback wouldn't + * be called. + * <p> + * It will be also called after the {@link #close()}, so you can put clean up code here. + * You don't need to call {@link #close()} after this. + */ + public void onDisconnected() { } + + /** + * Called when the session set the custom layout through the + * {@link MediaSession2#setCustomLayout(ControllerInfo, List)}. + * <p> + * Can be called before {@link #onConnected(CommandGroup)} is called. + * + * @param layout + */ + public void onCustomLayoutChanged(List<CommandButton> layout) { } + + /** + * Called when the session has changed anything related with the {@link PlaybackInfo}. + * + * @param info new playback info + */ + public void onPlaybackInfoChanged(PlaybackInfo info) { } + + /** + * Called when the allowed commands are changed by session. + * + * @param commands newly allowed commands + */ + public void onAllowedCommandsChanged(CommandGroup commands) { } + + /** + * Called when the session sent a custom command. + * + * @param command + * @param args + * @param receiver + */ + public void onCustomCommand(Command command, @Nullable Bundle args, + @Nullable ResultReceiver receiver) { } + + /** + * Called when the playlist is changed. + * + * @param playlist A new playlist set by the session. + */ + public void onPlaylistChanged(@NonNull List<MediaItem2> playlist) { } + + /** + * Called when the playback state is changed. + * + * @param state latest playback state + */ + public void onPlaybackStateChanged(@NonNull PlaybackState2 state) { } + + /** + * Called when the playlist parameters are changed. + * + * @param params The new play list parameters. + */ + public void onPlaylistParamsChanged(@NonNull PlaylistParams params) { } + } + + /** + * Holds information about the current playback and how audio is handled for + * this session. + */ + // The same as MediaController.PlaybackInfo + public static final class PlaybackInfo { + /** + * The session uses remote playback. + */ + public static final int PLAYBACK_TYPE_REMOTE = 2; + /** + * The session uses local playback. + */ + public static final int PLAYBACK_TYPE_LOCAL = 1; + + private final PlaybackInfoProvider mProvider; + + /** + * @hide + */ + @SystemApi + public PlaybackInfo(PlaybackInfoProvider provider) { + mProvider = provider; + } + + /** + * @hide + */ + @SystemApi + public PlaybackInfoProvider getProvider() { + return mProvider; + } + + /** + * Get the type of playback which affects volume handling. One of: + * <ul> + * <li>{@link #PLAYBACK_TYPE_LOCAL}</li> + * <li>{@link #PLAYBACK_TYPE_REMOTE}</li> + * </ul> + * + * @return The type of playback this session is using. + */ + public int getPlaybackType() { + return mProvider.getPlaybackType_impl(); + } + + /** + * Get the audio attributes for this session. The attributes will affect + * volume handling for the session. When the volume type is + * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the + * remote volume handler. + * + * @return The attributes for this session. + */ + public AudioAttributes getAudioAttributes() { + return mProvider.getAudioAttributes_impl(); + } + + /** + * Get the type of volume control that can be used. One of: + * <ul> + * <li>{@link VolumeProvider2#VOLUME_CONTROL_ABSOLUTE}</li> + * <li>{@link VolumeProvider2#VOLUME_CONTROL_RELATIVE}</li> + * <li>{@link VolumeProvider2#VOLUME_CONTROL_FIXED}</li> + * </ul> + * + * @return The type of volume control that may be used with this session. + */ + public int getControlType() { + return mProvider.getControlType_impl(); + } + + /** + * 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 mProvider.getMaxVolume_impl(); + } + + /** + * Get the current volume for this session. + * + * @return The current volume where this session is playing. + */ + public int getCurrentVolume() { + return mProvider.getCurrentVolume_impl(); + } + } + + private final MediaController2Provider mProvider; + + /** + * Create a {@link MediaController2} from the {@link SessionToken2}. This connects to the session + * and may wake up the service if it's not available. + * + * @param context Context + * @param token token to connect to + * @param executor executor to run callbacks on. + * @param callback controller callback to receive changes in + */ + // TODO(jaewan): Put @CallbackExecutor to the constructor. + public MediaController2(@NonNull Context context, @NonNull SessionToken2 token, + @NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) { + super(); + + mProvider = createProvider(context, token, executor, callback); + // This also connects to the token. + // Explicit connect() isn't added on purpose because retrying connect() is impossible with + // session whose session binder is only valid while it's active. + // prevent a controller from reusable after the + // session is released and recreated. + mProvider.initialize(); + } + + MediaController2Provider createProvider(@NonNull Context context, + @NonNull SessionToken2 token, @NonNull Executor executor, + @NonNull ControllerCallback callback) { + return ApiLoader.getProvider(context) + .createMediaController2(context, this, token, executor, callback); + } + + /** + * Release this object, and disconnect from the session. After this, callbacks wouldn't be + * received. + */ + @Override + public void close() { + mProvider.close_impl(); + } + + /** + * @hide + */ + @SystemApi + public MediaController2Provider getProvider() { + return mProvider; + } + + /** + * @return token + */ + public @NonNull + SessionToken2 getSessionToken() { + return mProvider.getSessionToken_impl(); + } + + /** + * Returns whether this class is connected to active {@link MediaSession2} or not. + */ + public boolean isConnected() { + return mProvider.isConnected_impl(); + } + + public void play() { + mProvider.play_impl(); + } + + public void pause() { + mProvider.pause_impl(); + } + + public void stop() { + mProvider.stop_impl(); + } + + public void skipToPrevious() { + mProvider.skipToPrevious_impl(); + } + + public void skipToNext() { + mProvider.skipToNext_impl(); + } + + /** + * Request that the player prepare its playback. In other words, other sessions can continue + * to play during the preparation of this session. This method can be used to speed up the + * start of the playback. Once the preparation is done, the session will change its playback + * state to {@link PlaybackState2#STATE_PAUSED}. Afterwards, {@link #play} can be called to + * start playback. + */ + public void prepare() { + mProvider.prepare_impl(); + } + + /** + * Start fast forwarding. If playback is already fast forwarding this + * may increase the rate. + */ + public void fastForward() { + mProvider.fastForward_impl(); + } + + /** + * Start rewinding. If playback is already rewinding this may increase + * the rate. + */ + public void rewind() { + mProvider.rewind_impl(); + } + + /** + * Move to a new location in the media stream. + * + * @param pos Position to move to, in milliseconds. + */ + public void seekTo(long pos) { + mProvider.seekTo_impl(pos); + } + + /** + * Sets the index of current DataSourceDesc in the play list to be played. + * + * @param index the index of DataSourceDesc in the play list you want to play + * @throws IllegalArgumentException if the play list is null + * @throws NullPointerException if index is outside play list range + */ + public void setCurrentPlaylistItem(int index) { + mProvider.setCurrentPlaylistItem_impl(index); + } + + /** + * Sets the {@link PlaylistParams} for the current play list. Repeat/shuffle mode and metadata + * for the list can be set by calling this method. + * + * @param params A {@link PlaylistParams} object to set. + * @throws IllegalArgumentException if given {@param param} is null. + */ + public void setPlaylistParams(PlaylistParams params) { + mProvider.setPlaylistParams_impl(params); + } + + /** + * @hide + */ + public void skipForward() { + // To match with KEYCODE_MEDIA_SKIP_FORWARD + } + + /** + * @hide + */ + public void skipBackward() { + // To match with KEYCODE_MEDIA_SKIP_BACKWARD + } + + /** + * Request that the player start playback for a specific media id. + * + * @param mediaId The id of the requested media. + * @param extras Optional extras that can include extra information about the media item + * to be played. + */ + public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) { + mProvider.playFromMediaId_impl(mediaId, extras); + } + + /** + * Request that the player start playback for a specific search query. + * An empty or null query should be treated as a request to play any + * music. + * + * @param query The search query. + * @param extras Optional extras that can include extra information + * about the query. + */ + public void playFromSearch(@NonNull String query, @Nullable Bundle extras) { + mProvider.playFromSearch_impl(query, extras); + } + + /** + * Request that the player start playback for a specific {@link Uri}. + * + * @param uri The URI of the requested media. + * @param extras Optional extras that can include extra information about the media item + * to be played. + */ + public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) { + mProvider.playFromUri_impl(uri, extras); + } + + + /** + * Request that the player prepare playback for a specific media id. In other words, other + * sessions can continue to play during the preparation of this session. This method can be + * used to speed up the start of the playback. Once the preparation is done, the session + * will change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards, + * {@link #play} can be called to start playback. If the preparation is not needed, + * {@link #playFromMediaId} can be directly called without this method. + * + * @param mediaId The id of the requested media. + * @param extras Optional extras that can include extra information about the media item + * to be prepared. + */ + public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) { + mProvider.prepareMediaId_impl(mediaId, extras); + } + + /** + * Request that the player prepare playback for a specific search query. An empty or null + * query should be treated as a request to prepare any music. In other words, other sessions + * can continue to play during the preparation of this session. This method can be used to + * speed up the start of the playback. Once the preparation is done, the session will + * change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards, + * {@link #play} can be called to start playback. If the preparation is not needed, + * {@link #playFromSearch} can be directly called without this method. + * + * @param query The search query. + * @param extras Optional extras that can include extra information + * about the query. + */ + public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) { + mProvider.prepareFromSearch_impl(query, extras); + } + + /** + * Request that the player prepare playback for a specific {@link Uri}. In other words, + * other sessions can continue to play during the preparation of this session. This method + * can be used to speed up the start of the playback. Once the preparation is done, the + * session will change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards, + * {@link #play} can be called to start playback. If the preparation is not needed, + * {@link #playFromUri} can be directly called without this method. + * + * @param uri The URI of the requested media. + * @param extras Optional extras that can include extra information about the media item + * to be prepared. + */ + public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) { + mProvider.prepareFromUri_impl(uri, extras); + } + + /** + * Set the volume of the output this session is playing on. The command will be ignored if it + * does not support {@link VolumeProvider2#VOLUME_CONTROL_ABSOLUTE}. + * <p> + * If the session is local playback, this changes the device's volume with the stream that + * session's player is using. Flags will be specified for the {@link AudioManager}. + * <p> + * If the session is remote player (i.e. session has set volume provider), its volume provider + * will receive this request instead. + * + * @see #getPlaybackInfo() + * @param value The value to set it to, between 0 and the reported max. + * @param flags flags from {@link AudioManager} to include with the volume request for local + * playback + */ + public void setVolumeTo(int value, int flags) { + mProvider.setVolumeTo_impl(value, flags); + } + + /** + * Adjust the volume of the output this session is playing on. The direction + * must be one of {@link AudioManager#ADJUST_LOWER}, + * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}. + * The command will be ignored if the session does not support + * {@link VolumeProvider2#VOLUME_CONTROL_RELATIVE} or + * {@link VolumeProvider2#VOLUME_CONTROL_ABSOLUTE}. + * <p> + * If the session is local playback, this changes the device's volume with the stream that + * session's player is using. Flags will be specified for the {@link AudioManager}. + * <p> + * If the session is remote player (i.e. session has set volume provider), its volume provider + * will receive this request instead. + * + * @see #getPlaybackInfo() + * @param direction The direction to adjust the volume in. + * @param flags flags from {@link AudioManager} to include with the volume request for local + * playback + */ + public void adjustVolume(int direction, int flags) { + mProvider.adjustVolume_impl(direction, flags); + } + + /** + * Get the rating type supported by the session. One of: + * <ul> + * <li>{@link Rating2#RATING_NONE}</li> + * <li>{@link Rating2#RATING_HEART}</li> + * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li> + * <li>{@link Rating2#RATING_3_STARS}</li> + * <li>{@link Rating2#RATING_4_STARS}</li> + * <li>{@link Rating2#RATING_5_STARS}</li> + * <li>{@link Rating2#RATING_PERCENTAGE}</li> + * </ul> + * + * @return The supported rating type + */ + public int getRatingType() { + return mProvider.getRatingType_impl(); + } + + /** + * Get an intent for launching UI associated with this session if one exists. + * + * @return A {@link PendingIntent} to launch UI or null. + */ + public @Nullable PendingIntent getSessionActivity() { + return mProvider.getSessionActivity_impl(); + } + + /** + * Get the lastly cached {@link PlaybackState2} from + * {@link ControllerCallback#onPlaybackStateChanged(PlaybackState2)}. + * <p> + * It may return {@code null} before the first callback or session has sent {@code null} + * playback state. + * + * @return a playback state. Can be {@code null} + */ + public @Nullable PlaybackState2 getPlaybackState() { + return mProvider.getPlaybackState_impl(); + } + + /** + * Get the current playback info for this session. + * + * @return The current playback info or null. + */ + public @Nullable PlaybackInfo getPlaybackInfo() { + return mProvider.getPlaybackInfo_impl(); + } + + /** + * Rate the current content. This will cause the rating to be set for + * the current user. The Rating type must match the type returned by + * {@link #getRatingType()}. + * + * @param rating The rating to set for the current content + */ + public void setRating(Rating2 rating) { + mProvider.setRating_impl(rating); + } + + /** + * Send custom command to the session + * + * @param command custom command + * @param args optional argument + * @param cb optional result receiver + */ + public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args, + @Nullable ResultReceiver cb) { + mProvider.sendCustomCommand_impl(command, args, cb); + } + + /** + * Return playlist from the session. + * + * @return playlist. Can be {@code null} if the controller doesn't have enough permission. + */ + public @Nullable List<MediaItem2> getPlaylist() { + return mProvider.getPlaylist_impl(); + } + + /** + * Returns the {@link PlaylistParams} for the current play list. + * Can return {@code null} if the controller doesn't have enough permission, or if the session + * has not set the parameters. + */ + public @Nullable PlaylistParams getPlaylistParams() { + return mProvider.getPlaylistParams_impl(); + } + + /** + * Removes the media item at index in the play list. + *<p> + * If index is same as the current index of the playlist, current playback + * will be stopped and playback moves to next source in the list. + * + * @return the removed DataSourceDesc at index in the play list + * @throws IllegalArgumentException if the play list is null + * @throws IndexOutOfBoundsException if index is outside play list range + */ + // TODO(jaewan): Remove with index was previously rejected by council (b/36524925) + // TODO(jaewan): Should we also add movePlaylistItem from index to index? + public void removePlaylistItem(MediaItem2 item) { + mProvider.removePlaylistItem_impl(item); + } + + /** + * Inserts the media item to the play list at position index. + * <p> + * This will not change the currently playing media item. + * If index is less than or equal to the current index of the play list, + * the current index of the play list will be incremented correspondingly. + * + * @param index the index you want to add dsd to the play list + * @param item the media item you want to add to the play list + * @throws IndexOutOfBoundsException if index is outside play list range + * @throws NullPointerException if dsd is null + */ + public void addPlaylistItem(int index, MediaItem2 item) { + mProvider.addPlaylistItem_impl(index, item); + } +} diff --git a/media/java/android/media/MediaDataSource.java b/media/java/android/media/MediaDataSource.java index 948da0b97910..4ba2120f5b09 100644 --- a/media/java/android/media/MediaDataSource.java +++ b/media/java/android/media/MediaDataSource.java @@ -34,8 +34,8 @@ public abstract class MediaDataSource implements Closeable { /** * Called to request data from the given position. * - * Implementations should should write up to {@code size} bytes into - * {@code buffer}, and return the number of bytes written. + * Implementations should fill {@code buffer} with up to {@code size} + * bytes of data, and return the number of valid bytes in the buffer. * * Return {@code 0} if size is zero (thus no bytes are read). * diff --git a/media/java/android/media/MediaDrm.java b/media/java/android/media/MediaDrm.java index 88b1c5ffcc7e..279e05f57d1c 100644 --- a/media/java/android/media/MediaDrm.java +++ b/media/java/android/media/MediaDrm.java @@ -16,13 +16,6 @@ package android.media; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.UUID; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -33,7 +26,18 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Parcel; +import android.os.PersistableBundle; import android.util.Log; +import dalvik.system.CloseGuard; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + /** * MediaDrm can be used to obtain keys for decrypting protected media streams, in @@ -91,10 +95,10 @@ import android.util.Log; * are only decrypted when the samples are delivered to the decoder. * <p> * MediaDrm methods throw {@link android.media.MediaDrm.MediaDrmStateException} - * when a method is called on a MediaDrm object that has had an unrecoverable failure - * in the DRM plugin or security hardware. - * {@link android.media.MediaDrm.MediaDrmStateException} extends - * {@link java.lang.IllegalStateException} with the addition of a developer-readable + * when a method is called on a MediaDrm object that has had an unrecoverable failure + * in the DRM plugin or security hardware. + * {@link android.media.MediaDrm.MediaDrmStateException} extends + * {@link java.lang.IllegalStateException} with the addition of a developer-readable * diagnostic information string associated with the exception. * <p> * In the event of a mediaserver process crash or restart while a MediaDrm object @@ -102,9 +106,9 @@ import android.util.Log; * To recover, the app must release the MediaDrm object, then create and initialize * a new one. * <p> - * As {@link android.media.MediaDrmResetException} and - * {@link android.media.MediaDrm.MediaDrmStateException} both extend - * {@link java.lang.IllegalStateException}, they should be in an earlier catch() + * As {@link android.media.MediaDrmResetException} and + * {@link android.media.MediaDrm.MediaDrmStateException} both extend + * {@link java.lang.IllegalStateException}, they should be in an earlier catch() * block than {@link java.lang.IllegalStateException} if handled separately. * <p> * <a name="Callbacks"></a> @@ -117,10 +121,13 @@ import android.util.Log; * MediaDrm objects on a thread with its own Looper running (main UI * thread by default has a Looper running). */ -public final class MediaDrm { +public final class MediaDrm implements AutoCloseable { private static final String TAG = "MediaDrm"; + private final AtomicBoolean mClosed = new AtomicBoolean(); + private final CloseGuard mCloseGuard = CloseGuard.get(); + private static final String PERMISSION = android.Manifest.permission.ACCESS_DRM_CERTIFICATES; private EventHandler mEventHandler; @@ -165,7 +172,7 @@ public final class MediaDrm { /** * Query if the given scheme identified by its UUID is supported on - * this device, and whether the drm plugin is able to handle the + * this device, and whether the DRM plugin is able to handle the * media container format specified by mimeType. * @param uuid The UUID of the crypto scheme. * @param mimeType The MIME type of the media container, e.g. "video/mp4" @@ -215,6 +222,8 @@ public final class MediaDrm { */ native_setup(new WeakReference<MediaDrm>(this), getByteArrayFromUUID(uuid), ActivityThread.currentOpPackageName()); + + mCloseGuard.open("release"); } /** @@ -625,8 +634,39 @@ public final class MediaDrm { * @throws ResourceBusyException if required resources are in use */ @NonNull - public native byte[] openSession() throws NotProvisionedException, - ResourceBusyException; + public byte[] openSession() throws NotProvisionedException, + ResourceBusyException { + return openSession(getMaxSecurityLevel()); + } + + /** + * Open a new session at a requested security level. The security level + * represents the robustness of the device's DRM implementation. By default, + * sessions are opened at the native security level of the device. + * Overriding the security level is necessary when the decrypted frames need + * to be manipulated, such as for image compositing. The security level + * parameter must be lower than the native level. Reducing the security + * level will typically limit the content to lower resolutions, as + * determined by the license policy. If the requested level is not + * supported, the next lower supported security level will be set. The level + * can be queried using {@link #getSecurityLevel}. A session + * ID is returned. + * + * @param level the new security level, one of + * {@link #SW_SECURE_CRYPTO}, {@link #SW_SECURE_DECODE}, + * {@link #HW_SECURE_CRYPTO}, {@link #HW_SECURE_DECODE} or + * {@link #HW_SECURE_ALL}. + * + * @throws NotProvisionedException if provisioning is needed + * @throws ResourceBusyException if required resources are in use + * @throws IllegalArgumentException if the requested security level is + * higher than the native level or lower than the lowest supported level or + * if the device does not support specifying the security level when opening + * a session + */ + @NonNull + public native byte[] openSession(@SecurityLevel int level) throws + NotProvisionedException, ResourceBusyException; /** * Close a session on the MediaDrm object that was previously opened @@ -670,12 +710,14 @@ public final class MediaDrm { private int mRequestType; /** - * Key request type is initial license request + * Key request type is initial license request. A license request + * is necessary to load keys. */ public static final int REQUEST_TYPE_INITIAL = 0; /** - * Key request type is license renewal + * Key request type is license renewal. A license request is + * necessary to prevent the keys from expiring. */ public static final int REQUEST_TYPE_RENEWAL = 1; @@ -684,11 +726,25 @@ public final class MediaDrm { */ public static final int REQUEST_TYPE_RELEASE = 2; + /** + * Keys are already loaded. No license request is necessary, and no + * key request data is returned. + */ + public static final int REQUEST_TYPE_NONE = 3; + + /** + * Keys have been loaded but an additional license request is needed + * to update their values. + */ + public static final int REQUEST_TYPE_UPDATE = 4; + /** @hide */ @IntDef({ REQUEST_TYPE_INITIAL, REQUEST_TYPE_RENEWAL, REQUEST_TYPE_RELEASE, + REQUEST_TYPE_NONE, + REQUEST_TYPE_UPDATE, }) @Retention(RetentionPolicy.SOURCE) public @interface RequestType {} @@ -729,7 +785,8 @@ public final class MediaDrm { /** * Get the type of the request * @return one of {@link #REQUEST_TYPE_INITIAL}, - * {@link #REQUEST_TYPE_RENEWAL} or {@link #REQUEST_TYPE_RELEASE} + * {@link #REQUEST_TYPE_RENEWAL}, {@link #REQUEST_TYPE_RELEASE}, + * {@link #REQUEST_TYPE_NONE} or {@link #REQUEST_TYPE_UPDATE} */ @RequestType public int getRequestType() { return mRequestType; } @@ -745,7 +802,7 @@ public final class MediaDrm { * returned in KeyRequest.defaultUrl. * <p> * After the app has received the key request response from the server, - * it should deliver to the response to the DRM engine plugin using the method + * it should deliver to the response to the MediaDrm instance using the method * {@link #provideKeyResponse}. * * @param scope may be a sessionId or a keySetId, depending on the specified keyType. @@ -781,7 +838,7 @@ public final class MediaDrm { /** * A key response is received from the license server by the app, then it is - * provided to the DRM engine plugin using provideKeyResponse. When the + * provided to the MediaDrm instance using provideKeyResponse. When the * response is for an offline key request, a keySetId is returned that can be * used to later restore the keys to a new session with the method * {@link #restoreKeys}. @@ -829,7 +886,7 @@ public final class MediaDrm { * in the form of {name, value} pairs. Since DRM license policies vary by vendor, * the specific status field names are determined by each DRM vendor. Refer to your * DRM provider documentation for definitions of the field names for a particular - * DRM engine plugin. + * DRM plugin. * * @param sessionId the session ID for the DRM session */ @@ -897,11 +954,11 @@ public final class MediaDrm { @NonNull String certAuthority); /** - * After a provision response is received by the app, it is provided to the DRM - * engine plugin using this method. + * After a provision response is received by the app, it is provided to the + * MediaDrm instance using this method. * * @param response the opaque provisioning response byte array to provide to the - * DRM engine plugin. + * MediaDrm instance. * * @throws DeniedByServerException if the response indicates that the * server rejected the request @@ -912,73 +969,270 @@ public final class MediaDrm { } @NonNull - /* could there be a valid response with 0-sized certificate or key? */ private native Certificate provideProvisionResponseNative(@NonNull byte[] response) throws DeniedByServerException; /** - * A means of enforcing limits on the number of concurrent streams per subscriber - * across devices is provided via SecureStop. This is achieved by securely - * monitoring the lifetime of sessions. + * Secure stops are a way to enforce limits on the number of concurrent + * streams per subscriber across devices. They provide secure monitoring of + * the lifetime of content decryption keys in MediaDrm sessions. * <p> - * Information from the server related to the current playback session is written - * to persistent storage on the device when each MediaCrypto object is created. + * A secure stop is written to secure persistent memory when keys are loaded + * into a MediaDrm session. The secure stop state indicates that the keys + * are available for use. When playback completes and the keys are removed + * or the session is destroyed, the secure stop state is updated to indicate + * that keys are no longer usable. * <p> - * In the normal case, playback will be completed, the session destroyed and the - * Secure Stops will be queried. The app queries secure stops and forwards the - * secure stop message to the server which verifies the signature and notifies the - * server side database that the session destruction has been confirmed. The persisted - * record on the client is only removed after positive confirmation that the server - * received the message using releaseSecureStops(). + * After playback, the app can query the secure stop and send it in a + * message to the license server confirming that the keys are no longer + * active. The license server returns a secure stop release response + * message to the app which then deletes the secure stop from persistent + * memory using {@link #releaseSecureStops}. + * @return a list of all secure stops from secure persistent memory */ @NonNull public native List<byte[]> getSecureStops(); /** - * Access secure stop by secure stop ID. + * Return a list of all secure stop IDs currently in persistent memory. * - * @param ssid - The secure stop ID provided by the license server. + * @return a list of secure stop IDs + */ + @NonNull + public native List<byte[]> getSecureStopIds(); + + /** + * Access a specific secure stop given its secure stop ID. + * + * @param ssid the ID of the secure stop to return + * @return the secure stop identified by ssid */ @NonNull public native byte[] getSecureStop(@NonNull byte[] ssid); /** - * Process the SecureStop server response message ssRelease. After authenticating - * the message, remove the SecureStops identified in the response. + * Process the secure stop server response message ssRelease. After + * authenticating the message, remove the secure stops identified in the + * response. * * @param ssRelease the server response indicating which secure stops to release */ public native void releaseSecureStops(@NonNull byte[] ssRelease); /** - * Remove all secure stops without requiring interaction with the server. + * Remove a specific secure stop without requiring a secure stop release message + * from the license server. + * @param ssid the ID of the secure stop to remove + */ + public native void removeSecureStop(@NonNull byte[] ssid); + + /** + * Remove all secure stops without requiring a secure stop release message from + * the license server. + * + * This method was added in API 28. In API versions 18 through 27, + * {@link #releaseAllSecureStops} should be called instead. There is no need to + * do anything for API versions prior to 18. + */ + public native void removeAllSecureStops(); + + /** + * Remove all secure stops without requiring a secure stop release message from + * the license server. + * + * @deprecated Remove all secure stops using {@link #removeAllSecureStops} instead. + */ + public void releaseAllSecureStops() { + removeAllSecureStops();; + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({HDCP_LEVEL_UNKNOWN, HDCP_NONE, HDCP_V1, HDCP_V2, + HDCP_V2_1, HDCP_V2_2, HDCP_NO_DIGITAL_OUTPUT}) + public @interface HdcpLevel {} + + + /** + * The DRM plugin did not report an HDCP level, or an error + * occurred accessing it + */ + public static final int HDCP_LEVEL_UNKNOWN = 0; + + /** + * HDCP is not supported on this device, content is unprotected + */ + public static final int HDCP_NONE = 1; + + /** + * HDCP version 1.0 + */ + public static final int HDCP_V1 = 2; + + /** + * HDCP version 2.0 Type 1. */ - public native void releaseAllSecureStops(); + public static final int HDCP_V2 = 3; /** - * String property name: identifies the maker of the DRM engine plugin + * HDCP version 2.1 Type 1. + */ + public static final int HDCP_V2_1 = 4; + + /** + * HDCP version 2.2 Type 1. + */ + public static final int HDCP_V2_2 = 5; + + /** + * No digital output, implicitly secure + */ + public static final int HDCP_NO_DIGITAL_OUTPUT = Integer.MAX_VALUE; + + /** + * Return the HDCP level negotiated with downstream receivers the + * device is connected to. If multiple HDCP-capable displays are + * simultaneously connected to separate interfaces, this method + * returns the lowest negotiated level of all interfaces. + * <p> + * This method should only be used for informational purposes, not for + * enforcing compliance with HDCP requirements. Trusted enforcement of + * HDCP policies must be handled by the DRM system. + * <p> + * @return one of {@link #HDCP_LEVEL_UNKNOWN}, {@link #HDCP_NONE}, + * {@link #HDCP_V1}, {@link #HDCP_V2}, {@link #HDCP_V2_1}, {@link #HDCP_V2_2} + * or {@link #HDCP_NO_DIGITAL_OUTPUT}. + */ + @HdcpLevel + public native int getConnectedHdcpLevel(); + + /** + * Return the maximum supported HDCP level. The maximum HDCP level is a + * constant for a given device, it does not depend on downstream receivers + * that may be connected. If multiple HDCP-capable interfaces are present, + * it indicates the highest of the maximum HDCP levels of all interfaces. + * <p> + * @return one of {@link #HDCP_LEVEL_UNKNOWN}, {@link #HDCP_NONE}, + * {@link #HDCP_V1}, {@link #HDCP_V2}, {@link #HDCP_V2_1}, {@link #HDCP_V2_2} + * or {@link #HDCP_NO_DIGITAL_OUTPUT}. + */ + @HdcpLevel + public native int getMaxHdcpLevel(); + + /** + * Return the number of MediaDrm sessions that are currently opened + * simultaneously among all MediaDrm instances for the active DRM scheme. + * @return the number of open sessions. + */ + public native int getOpenSessionCount(); + + /** + * Return the maximum number of MediaDrm sessions that may be opened + * simultaneosly among all MediaDrm instances for the active DRM + * scheme. The maximum number of sessions is not affected by any + * sessions that may have already been opened. + * @return maximum sessions. + */ + public native int getMaxSessionCount(); + + /** + * Security level indicates the robustness of the device's DRM + * implementation. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURITY_LEVEL_UNKNOWN, SW_SECURE_CRYPTO, SW_SECURE_DECODE, + HW_SECURE_CRYPTO, HW_SECURE_DECODE, HW_SECURE_ALL}) + public @interface SecurityLevel {} + + /** + * The DRM plugin did not report a security level, or an error occurred + * accessing it + */ + public static final int SECURITY_LEVEL_UNKNOWN = 0; + + /** + * DRM key management uses software-based whitebox crypto. + */ + public static final int SW_SECURE_CRYPTO = 1; + + /** + * DRM key management and decoding use software-based whitebox crypto. + */ + public static final int SW_SECURE_DECODE = 2; + + /** + * DRM key management and crypto operations are performed within a hardware + * backed trusted execution environment. + */ + public static final int HW_SECURE_CRYPTO = 3; + + /** + * DRM key management, crypto operations and decoding of content are + * performed within a hardware backed trusted execution environment. + */ + public static final int HW_SECURE_DECODE = 4; + + /** + * DRM key management, crypto operations, decoding of content and all + * handling of the media (compressed and uncompressed) is handled within a + * hardware backed trusted execution environment. + */ + public static final int HW_SECURE_ALL = 5; + + /** + * The maximum security level supported by the device. This is the default + * security level when a session is opened. + * @hide + */ + public static final int SECURITY_LEVEL_MAX = 6; + + /** + * The maximum security level supported by the device. This is the default + * security level when a session is opened. + */ + @SecurityLevel + public static final int getMaxSecurityLevel() { + return SECURITY_LEVEL_MAX; + } + + /** + * Return the current security level of a session. A session has an initial + * security level determined by the robustness of the DRM system's + * implementation on the device. The security level may be changed at the + * time a session is opened using {@link #openSession}. + * @param sessionId the session to query. + * <p> + * @return one of {@link #SECURITY_LEVEL_UNKNOWN}, + * {@link #SW_SECURE_CRYPTO}, {@link #SW_SECURE_DECODE}, + * {@link #HW_SECURE_CRYPTO}, {@link #HW_SECURE_DECODE} or + * {@link #HW_SECURE_ALL}. + */ + @SecurityLevel + public native int getSecurityLevel(@NonNull byte[] sessionId); + + /** + * String property name: identifies the maker of the DRM plugin */ public static final String PROPERTY_VENDOR = "vendor"; /** - * String property name: identifies the version of the DRM engine plugin + * String property name: identifies the version of the DRM plugin */ public static final String PROPERTY_VERSION = "version"; /** - * String property name: describes the DRM engine plugin + * String property name: describes the DRM plugin */ public static final String PROPERTY_DESCRIPTION = "description"; /** * String property name: a comma-separated list of cipher and mac algorithms - * supported by CryptoSession. The list may be empty if the DRM engine + * supported by CryptoSession. The list may be empty if the DRM * plugin does not support CryptoSession operations. */ public static final String PROPERTY_ALGORITHMS = "algorithms"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "PROPERTY_" }, value = { PROPERTY_VENDOR, PROPERTY_VERSION, PROPERTY_DESCRIPTION, @@ -988,32 +1242,37 @@ public final class MediaDrm { public @interface StringProperty {} /** - * Read a DRM engine plugin String property value, given the property name string. + * Read a MediaDrm String property value, given the property name string. * <p> * Standard fields names are: * {@link #PROPERTY_VENDOR}, {@link #PROPERTY_VERSION}, * {@link #PROPERTY_DESCRIPTION}, {@link #PROPERTY_ALGORITHMS} */ - /* FIXME this throws IllegalStateException for invalid property names */ @NonNull public native String getPropertyString(@NonNull @StringProperty String propertyName); /** + * Set a MediaDrm String property value, given the property name string + * and new value for the property. + */ + public native void setPropertyString(@NonNull @StringProperty String propertyName, + @NonNull String value); + + /** * Byte array property name: the device unique identifier is established during * device provisioning and provides a means of uniquely identifying each device. */ - /* FIXME this throws IllegalStateException for invalid property names */ public static final String PROPERTY_DEVICE_UNIQUE_ID = "deviceUniqueId"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "PROPERTY_" }, value = { PROPERTY_DEVICE_UNIQUE_ID, }) @Retention(RetentionPolicy.SOURCE) public @interface ArrayProperty {} /** - * Read a DRM engine plugin byte array property value, given the property name string. + * Read a MediaDrm byte array property value, given the property name string. * <p> * Standard fields names are {@link #PROPERTY_DEVICE_UNIQUE_ID} */ @@ -1021,15 +1280,10 @@ public final class MediaDrm { public native byte[] getPropertyByteArray(@ArrayProperty String propertyName); /** - * Set a DRM engine plugin String property value. - */ - public native void setPropertyString( - String propertyName, @NonNull String value); - - /** - * Set a DRM engine plugin byte array property value. - */ - public native void setPropertyByteArray( + * Set a MediaDrm byte array property value, given the property name string + * and new value for the property. + */ + public native void setPropertyByteArray(@NonNull @ArrayProperty String propertyName, @NonNull byte[] value); private static final native void setCipherAlgorithmNative( @@ -1058,6 +1312,23 @@ public final class MediaDrm { @NonNull byte[] keyId, @NonNull byte[] message, @NonNull byte[] signature); /** + * Return Metrics data about the current MediaDrm instance. + * + * @return a {@link PersistableBundle} containing the set of attributes and values + * available for this instance of MediaDrm. + * The attributes are described in {@link MetricsConstants}. + * + * Additional vendor-specific fields may also be present in + * the return value. + */ + public PersistableBundle getMetrics() { + PersistableBundle bundle = getMetricsNative(); + return bundle; + } + + private native PersistableBundle getMetricsNative(); + + /** * In addition to supporting decryption of DASH Common Encrypted Media, the * MediaDrm APIs provide the ability to securely deliver session keys from * an operator's session key server to a client device, based on the factory-installed @@ -1160,7 +1431,7 @@ public final class MediaDrm { * The algorithm string conforms to JCA Standard Names for Mac * Algorithms and is case insensitive. For example "HmacSHA256". * <p> - * The list of supported algorithms for a DRM engine plugin can be obtained + * The list of supported algorithms for a DRM plugin can be obtained * using the method {@link #getPropertyString} with the property name * "algorithms". */ @@ -1274,7 +1545,7 @@ public final class MediaDrm { * storage, and used when invoking the signRSA method. * * @param response the opaque certificate response byte array to provide to the - * DRM engine plugin. + * MediaDrm instance. * * @throws DeniedByServerException if the response indicates that the * server rejected the request @@ -1311,20 +1582,413 @@ public final class MediaDrm { } @Override - protected void finalize() { - native_finalize(); + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + release(); + } finally { + super.finalize(); + } } - public native final void release(); + /** + * Releases resources associated with the current session of + * MediaDrm. It is considered good practice to call this method when + * the {@link MediaDrm} object is no longer needed in your + * application. After this method is called, {@link MediaDrm} is no + * longer usable since it has lost all of its required resource. + * + * This method was added in API 28. In API versions 18 through 27, release() + * should be called instead. There is no need to do anything for API + * versions prior to 18. + */ + @Override + public void close() { + release(); + } + + /** + * @deprecated replaced by {@link #close()}. + */ + @Deprecated + public void release() { + mCloseGuard.close(); + if (mClosed.compareAndSet(false, true)) { + native_release(); + } + } + + /** @hide */ + public native final void native_release(); + private static native final void native_init(); private native final void native_setup(Object mediadrm_this, byte[] uuid, String appPackageName); - private native final void native_finalize(); - static { System.loadLibrary("media_jni"); native_init(); } + + /** + * Definitions for the metrics that are reported via the + * {@link #getMetrics} call. + */ + public final static class MetricsConstants + { + private MetricsConstants() {} + + /** + * Key to extract the number of successful {@link #openSession} calls + * from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String OPEN_SESSION_OK_COUNT + = "drm.mediadrm.open_session.ok.count"; + + /** + * Key to extract the number of failed {@link #openSession} calls + * from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String OPEN_SESSION_ERROR_COUNT + = "drm.mediadrm.open_session.error.count"; + + /** + * Key to extract the list of error codes that were returned from + * {@link #openSession} calls. The key is used to lookup the list + * in the {@link PersistableBundle} returned by a {@link #getMetrics} + * call. + * The list is an array of Long values + * ({@link android.os.BaseBundle#getLongArray}). + */ + public static final String OPEN_SESSION_ERROR_LIST + = "drm.mediadrm.open_session.error.list"; + + /** + * Key to extract the number of successful {@link #closeSession} calls + * from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String CLOSE_SESSION_OK_COUNT + = "drm.mediadrm.close_session.ok.count"; + + /** + * Key to extract the number of failed {@link #closeSession} calls + * from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String CLOSE_SESSION_ERROR_COUNT + = "drm.mediadrm.close_session.error.count"; + + /** + * Key to extract the list of error codes that were returned from + * {@link #closeSession} calls. The key is used to lookup the list + * in the {@link PersistableBundle} returned by a {@link #getMetrics} + * call. + * The list is an array of Long values + * ({@link android.os.BaseBundle#getLongArray}). + */ + public static final String CLOSE_SESSION_ERROR_LIST + = "drm.mediadrm.close_session.error.list"; + + /** + * Key to extract the start times of sessions. Times are + * represented as milliseconds since epoch (1970-01-01T00:00:00Z). + * The start times are returned from the {@link PersistableBundle} + * from a {@link #getMetrics} call. + * The start times are returned as another {@link PersistableBundle} + * containing the session ids as keys and the start times as long + * values. Use {@link android.os.BaseBundle#keySet} to get the list of + * session ids, and then {@link android.os.BaseBundle#getLong} to get + * the start time for each session. + */ + public static final String SESSION_START_TIMES_MS + = "drm.mediadrm.session_start_times_ms"; + + /** + * Key to extract the end times of sessions. Times are + * represented as milliseconds since epoch (1970-01-01T00:00:00Z). + * The end times are returned from the {@link PersistableBundle} + * from a {@link #getMetrics} call. + * The end times are returned as another {@link PersistableBundle} + * containing the session ids as keys and the end times as long + * values. Use {@link android.os.BaseBundle#keySet} to get the list of + * session ids, and then {@link android.os.BaseBundle#getLong} to get + * the end time for each session. + */ + public static final String SESSION_END_TIMES_MS + = "drm.mediadrm.session_end_times_ms"; + + /** + * Key to extract the number of successful {@link #getKeyRequest} calls + * from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String GET_KEY_REQUEST_OK_COUNT + = "drm.mediadrm.get_key_request.ok.count"; + + /** + * Key to extract the number of failed {@link #getKeyRequest} + * calls from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String GET_KEY_REQUEST_ERROR_COUNT + = "drm.mediadrm.get_key_request.error.count"; + + /** + * Key to extract the list of error codes that were returned from + * {@link #getKeyRequest} calls. The key is used to lookup the list + * in the {@link PersistableBundle} returned by a {@link #getMetrics} + * call. + * The list is an array of Long values + * ({@link android.os.BaseBundle#getLongArray}). + */ + public static final String GET_KEY_REQUEST_ERROR_LIST + = "drm.mediadrm.get_key_request.error.list"; + + /** + * Key to extract the average time in microseconds of calls to + * {@link #getKeyRequest}. The value is retrieved from the + * {@link PersistableBundle} returned from {@link #getMetrics}. + * The time is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String GET_KEY_REQUEST_OK_TIME_MICROS + = "drm.mediadrm.get_key_request.ok.average_time_micros"; + + /** + * Key to extract the number of successful {@link #provideKeyResponse} + * calls from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String PROVIDE_KEY_RESPONSE_OK_COUNT + = "drm.mediadrm.provide_key_response.ok.count"; + + /** + * Key to extract the number of failed {@link #provideKeyResponse} + * calls from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String PROVIDE_KEY_RESPONSE_ERROR_COUNT + = "drm.mediadrm.provide_key_response.error.count"; + + /** + * Key to extract the list of error codes that were returned from + * {@link #provideKeyResponse} calls. The key is used to lookup the + * list in the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The list is an array of Long values + * ({@link android.os.BaseBundle#getLongArray}). + */ + public static final String PROVIDE_KEY_RESPONSE_ERROR_LIST + = "drm.mediadrm.provide_key_response.error.list"; + + /** + * Key to extract the average time in microseconds of calls to + * {@link #provideKeyResponse}. The valus is retrieved from the + * {@link PersistableBundle} returned from {@link #getMetrics}. + * The time is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String PROVIDE_KEY_RESPONSE_OK_TIME_MICROS + = "drm.mediadrm.provide_key_response.ok.average_time_micros"; + + /** + * Key to extract the number of successful {@link #getProvisionRequest} + * calls from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String GET_PROVISION_REQUEST_OK_COUNT + = "drm.mediadrm.get_provision_request.ok.count"; + + /** + * Key to extract the number of failed {@link #getProvisionRequest} + * calls from the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String GET_PROVISION_REQUEST_ERROR_COUNT + = "drm.mediadrm.get_provision_request.error.count"; + + /** + * Key to extract the list of error codes that were returned from + * {@link #getProvisionRequest} calls. The key is used to lookup the + * list in the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The list is an array of Long values + * ({@link android.os.BaseBundle#getLongArray}). + */ + public static final String GET_PROVISION_REQUEST_ERROR_LIST + = "drm.mediadrm.get_provision_request.error.list"; + + /** + * Key to extract the number of successful + * {@link #provideProvisionResponse} calls from the + * {@link PersistableBundle} returned by a {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String PROVIDE_PROVISION_RESPONSE_OK_COUNT + = "drm.mediadrm.provide_provision_response.ok.count"; + + /** + * Key to extract the number of failed + * {@link #provideProvisionResponse} calls from the + * {@link PersistableBundle} returned by a {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String PROVIDE_PROVISION_RESPONSE_ERROR_COUNT + = "drm.mediadrm.provide_provision_response.error.count"; + + /** + * Key to extract the list of error codes that were returned from + * {@link #provideProvisionResponse} calls. The key is used to lookup + * the list in the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The list is an array of Long values + * ({@link android.os.BaseBundle#getLongArray}). + */ + public static final String PROVIDE_PROVISION_RESPONSE_ERROR_LIST + = "drm.mediadrm.provide_provision_response.error.list"; + + /** + * Key to extract the number of successful + * {@link #getPropertyByteArray} calls were made with the + * {@link #PROPERTY_DEVICE_UNIQUE_ID} value. The key is used to lookup + * the value in the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String GET_DEVICE_UNIQUE_ID_OK_COUNT + = "drm.mediadrm.get_device_unique_id.ok.count"; + + /** + * Key to extract the number of failed + * {@link #getPropertyByteArray} calls were made with the + * {@link #PROPERTY_DEVICE_UNIQUE_ID} value. The key is used to lookup + * the value in the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String GET_DEVICE_UNIQUE_ID_ERROR_COUNT + = "drm.mediadrm.get_device_unique_id.error.count"; + + /** + * Key to extract the list of error codes that were returned from + * {@link #getPropertyByteArray} calls with the + * {@link #PROPERTY_DEVICE_UNIQUE_ID} value. The key is used to lookup + * the list in the {@link PersistableBundle} returned by a + * {@link #getMetrics} call. + * The list is an array of Long values + * ({@link android.os.BaseBundle#getLongArray}). + */ + public static final String GET_DEVICE_UNIQUE_ID_ERROR_LIST + = "drm.mediadrm.get_device_unique_id.error.list"; + + /** + * Key to extraact the count of {@link KeyStatus#STATUS_EXPIRED} events + * that occured. The count is extracted from the + * {@link PersistableBundle} returned from a {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String KEY_STATUS_EXPIRED_COUNT + = "drm.mediadrm.key_status.EXPIRED.count"; + + /** + * Key to extract the count of {@link KeyStatus#STATUS_INTERNAL_ERROR} + * events that occured. The count is extracted from the + * {@link PersistableBundle} returned from a {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String KEY_STATUS_INTERNAL_ERROR_COUNT + = "drm.mediadrm.key_status.INTERNAL_ERROR.count"; + + /** + * Key to extract the count of + * {@link KeyStatus#STATUS_OUTPUT_NOT_ALLOWED} events that occured. + * The count is extracted from the + * {@link PersistableBundle} returned from a {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String KEY_STATUS_OUTPUT_NOT_ALLOWED_COUNT + = "drm.mediadrm.key_status_change.OUTPUT_NOT_ALLOWED.count"; + + /** + * Key to extract the count of {@link KeyStatus#STATUS_PENDING} + * events that occured. The count is extracted from the + * {@link PersistableBundle} returned from a {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String KEY_STATUS_PENDING_COUNT + = "drm.mediadrm.key_status_change.PENDING.count"; + + /** + * Key to extract the count of {@link KeyStatus#STATUS_USABLE} + * events that occured. The count is extracted from the + * {@link PersistableBundle} returned from a {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String KEY_STATUS_USABLE_COUNT + = "drm.mediadrm.key_status_change.USABLE.count"; + + /** + * Key to extract the count of {@link OnEventListener#onEvent} + * calls of type PROVISION_REQUIRED occured. The count is + * extracted from the {@link PersistableBundle} returned from a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String EVENT_PROVISION_REQUIRED_COUNT + = "drm.mediadrm.event.PROVISION_REQUIRED.count"; + + /** + * Key to extract the count of {@link OnEventListener#onEvent} + * calls of type KEY_NEEDED occured. The count is + * extracted from the {@link PersistableBundle} returned from a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String EVENT_KEY_NEEDED_COUNT + = "drm.mediadrm.event.KEY_NEEDED.count"; + + /** + * Key to extract the count of {@link OnEventListener#onEvent} + * calls of type KEY_EXPIRED occured. The count is + * extracted from the {@link PersistableBundle} returned from a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String EVENT_KEY_EXPIRED_COUNT + = "drm.mediadrm.event.KEY_EXPIRED.count"; + + /** + * Key to extract the count of {@link OnEventListener#onEvent} + * calls of type VENDOR_DEFINED. The count is + * extracted from the {@link PersistableBundle} returned from a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String EVENT_VENDOR_DEFINED_COUNT + = "drm.mediadrm.event.VENDOR_DEFINED.count"; + + /** + * Key to extract the count of {@link OnEventListener#onEvent} + * calls of type SESSION_RECLAIMED. The count is + * extracted from the {@link PersistableBundle} returned from a + * {@link #getMetrics} call. + * The count is a Long value ({@link android.os.BaseBundle#getLong}). + */ + public static final String EVENT_SESSION_RECLAIMED_COUNT + = "drm.mediadrm.event.SESSION_RECLAIMED.count"; + } } diff --git a/media/java/android/media/MediaExtractor.java b/media/java/android/media/MediaExtractor.java index 2c1b4b3526e0..4919eeb4dacb 100644 --- a/media/java/android/media/MediaExtractor.java +++ b/media/java/android/media/MediaExtractor.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; +import android.media.AudioPresentation; import android.media.MediaCodec; import android.media.MediaFormat; import android.media.MediaHTTPService; @@ -40,6 +41,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -396,6 +398,17 @@ final public class MediaExtractor { } /** + * Get the list of available audio presentations for the track. + * @param trackIndex index of the track. + * @return a list of available audio presentations for a given valid audio track index. + * The list will be empty if the source does not contain any audio presentations. + */ + @NonNull + public List<AudioPresentation> getAudioPresentations(int trackIndex) { + return new ArrayList<AudioPresentation>(); + } + + /** * Get the PSSH info if present. * @return a map of uuid-to-bytes, with the uuid specifying * the crypto scheme, and the bytes being the data specific to that scheme. @@ -626,6 +639,12 @@ final public class MediaExtractor { */ public native long getSampleTime(); + /** + * @return size of the current sample in bytes or -1 if no more + * samples are available. + */ + public native long getSampleSize(); + // Keep these in sync with their equivalents in NuMediaExtractor.h /** * The sample is a sync sample (or in {@link MediaCodec}'s terminology diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index ed5f7d848663..e9128e4c827d 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -96,6 +96,19 @@ import java.util.Map; * <tr><td>{@link #KEY_MIME}</td><td>String</td><td>The type of the format.</td></tr> * <tr><td>{@link #KEY_LANGUAGE}</td><td>String</td><td>The language of the content.</td></tr> * </table> + * + * Image formats have the following keys: + * <table> + * <tr><td>{@link #KEY_MIME}</td><td>String</td><td>The type of the format.</td></tr> + * <tr><td>{@link #KEY_WIDTH}</td><td>Integer</td><td></td></tr> + * <tr><td>{@link #KEY_HEIGHT}</td><td>Integer</td><td></td></tr> + * <tr><td>{@link #KEY_COLOR_FORMAT}</td><td>Integer</td><td>set by the user + * for encoders, readable in the output format of decoders</b></td></tr> + * <tr><td>{@link #KEY_GRID_WIDTH}</td><td>Integer</td><td>required if the image has grid</td></tr> + * <tr><td>{@link #KEY_GRID_HEIGHT}</td><td>Integer</td><td>required if the image has grid</td></tr> + * <tr><td>{@link #KEY_GRID_ROWS}</td><td>Integer</td><td>required if the image has grid</td></tr> + * <tr><td>{@link #KEY_GRID_COLS}</td><td>Integer</td><td>required if the image has grid</td></tr> + * </table> */ public final class MediaFormat { public static final String MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8"; @@ -126,6 +139,35 @@ public final class MediaFormat { public static final String MIMETYPE_AUDIO_SCRAMBLED = "audio/scrambled"; /** + * MIME type for HEIF still image data encoded in HEVC. + * + * To decode such an image, {@link MediaCodec} decoder for + * {@ #MIMETYPE_VIDEO_HEVC} shall be used. The client needs to form + * the correct {@link #MediaFormat} based on additional information in + * the track format, and send it to {@link MediaCodec#configure}. + * + * The track's MediaFormat will come with {@link #KEY_WIDTH} and + * {@link #KEY_HEIGHT} keys, which describes the width and height + * of the image. If the image doesn't contain grid (i.e. none of + * {@link #KEY_GRID_WIDTH}, {@link #KEY_GRID_HEIGHT}, + * {@link #KEY_GRID_ROWS}, {@link #KEY_GRID_COLS} are present}), the + * track will contain a single sample of coded data for the entire image, + * and the image width and height should be used to set up the decoder. + * + * If the image does come with grid, each sample from the track will + * contain one tile in the grid, of which the size is described by + * {@link #KEY_GRID_WIDTH} and {@link #KEY_GRID_HEIGHT}. This size + * (instead of {@link #KEY_WIDTH} and {@link #KEY_HEIGHT}) should be + * used to set up the decoder. The track contains {@link #KEY_GRID_ROWS} + * by {@link #KEY_GRID_COLS} samples in row-major, top-row first, + * left-to-right order. The output image should be reconstructed by + * first tiling the decoding results of the tiles in the correct order, + * then trimming (before rotation is applied) on the bottom and right + * side, if the tiled area is larger than the image width and height. + */ + public static final String MIMETYPE_IMAGE_ANDROID_HEIC = "image/vnd.android.heic"; + + /** * MIME type for WebVTT subtitle data. */ public static final String MIMETYPE_TEXT_VTT = "text/vtt"; @@ -232,6 +274,54 @@ public final class MediaFormat { public static final String KEY_FRAME_RATE = "frame-rate"; /** + * A key describing the grid width of the content in a {@link #MIMETYPE_IMAGE_ANDROID_HEIC} + * track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_GRID_HEIGHT + * @see #KEY_GRID_ROWS + * @see #KEY_GRID_COLS + */ + public static final String KEY_GRID_WIDTH = "grid-width"; + + /** + * A key describing the grid height of the content in a {@link #MIMETYPE_IMAGE_ANDROID_HEIC} + * track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_GRID_WIDTH + * @see #KEY_GRID_ROWS + * @see #KEY_GRID_COLS + */ + public static final String KEY_GRID_HEIGHT = "grid-height"; + + /** + * A key describing the number of grid rows in the content in a + * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_GRID_WIDTH + * @see #KEY_GRID_HEIGHT + * @see #KEY_GRID_COLS + */ + public static final String KEY_GRID_ROWS = "grid-rows"; + + /** + * A key describing the number of grid columns in the content in a + * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_GRID_WIDTH + * @see #KEY_GRID_HEIGHT + * @see #KEY_GRID_ROWS + */ + public static final String KEY_GRID_COLS = "grid-cols"; + + /** * A key describing the raw audio sample encoding/format. * * <p>The associated value is an integer, using one of the @@ -511,8 +601,6 @@ public final class MediaFormat { * codec specific, but lower values generally result in more efficient * (smaller-sized) encoding. * - * @hide - * * @see MediaCodecInfo.EncoderCapabilities#getQualityRange() */ public static final String KEY_QUALITY = "quality"; @@ -590,6 +678,21 @@ public final class MediaFormat { public static final String KEY_LATENCY = "latency"; /** + * An optional key describing the maximum number of non-display-order coded frames. + * This is an optional parameter that applies only to video encoders. Application should + * check the value for this key in the output format to see if codec will produce + * non-display-order coded frames. If encoder supports it, the output frames' order will be + * different from the display order and each frame's display order could be retrived from + * {@link MediaCodec.BufferInfo#presentationTimeUs}. Before API level 27, application may + * receive non-display-order coded frames even though the application did not request it. + * Note: Application should not rearrange the frames to display order before feeding them + * to {@link MediaMuxer#writeSampleData}. + * <p> + * The default value is 0. + */ + public static final String KEY_OUTPUT_REORDER_DEPTH = "output-reorder-depth"; + + /** * A key describing the desired clockwise rotation on an output surface. * This key is only used when the codec is configured using an output surface. * The associated value is an integer, representing degrees. Supported values @@ -631,14 +734,16 @@ public final class MediaFormat { /** * A key for boolean DEFAULT behavior for the track. The track with DEFAULT=true is * selected in the absence of a specific user choice. - * This is currently only used for subtitle tracks, when the user selected - * 'Default' for the captioning locale. + * This is currently used in two scenarios: + * 1) for subtitle tracks, when the user selected 'Default' for the captioning locale. + * 2) for a {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track, indicating the image is the + * primary item in the file. + * The associated value is an integer, where non-0 means TRUE. This is an optional * field; if not specified, DEFAULT is considered to be FALSE. */ public static final String KEY_IS_DEFAULT = "is-default"; - /** * A key for the FORCED field for subtitle tracks. True if it is a * forced subtitle track. Forced subtitle tracks are essential for the diff --git a/media/java/android/media/MediaItem2.java b/media/java/android/media/MediaItem2.java new file mode 100644 index 000000000000..667aac1b426a --- /dev/null +++ b/media/java/android/media/MediaItem2.java @@ -0,0 +1,159 @@ +/* + * Copyright 2018 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.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.media.update.ApiLoader; +import android.media.update.MediaItem2Provider; +import android.os.Bundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A class with information on a single media item with the metadata information. + * Media item are application dependent so we cannot guarantee that they contain the right values. + * <p> + * When it's sent to a controller or browser, it's anonymized and data descriptor wouldn't be sent. + * <p> + * This object isn't a thread safe. + * @hide + */ +public class MediaItem2 { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE }) + public @interface Flags { } + + /** + * Flag: Indicates that the item has children of its own. + */ + public static final int FLAG_BROWSABLE = 1 << 0; + + /** + * Flag: Indicates that the item is playable. + * <p> + * The id of this item may be passed to + * {@link MediaController2#playFromMediaId(String, Bundle)} + */ + public static final int FLAG_PLAYABLE = 1 << 1; + + private final MediaItem2Provider mProvider; + + /** + * Create a new media item. + * + * @param mediaId id of this item. It must be unique whithin this app + * @param metadata metadata with the media id. + * @param flags The flags for this item. + */ + public MediaItem2(@NonNull Context context, @NonNull String mediaId, + @NonNull DataSourceDesc dsd, @Nullable MediaMetadata2 metadata, + @Flags int flags) { + mProvider = ApiLoader.getProvider(context).createMediaItem2( + context, this, mediaId, dsd, metadata, flags); + } + + /** + * Create a new media item + * @hide + */ + @SystemApi + public MediaItem2(MediaItem2Provider provider) { + mProvider = provider; + } + + /** + * Return this object as a bundle to share between processes. + * + * @return a new bundle instance + */ + public Bundle toBundle() { + // TODO(jaewan): Fill here when we rebase. + return mProvider.toBundle_impl(); + } + + public static MediaItem2 fromBundle(Context context, Bundle bundle) { + return ApiLoader.getProvider(context).fromBundle_MediaItem2(context, bundle); + } + + public String toString() { + return mProvider.toString_impl(); + } + + /** + * Gets the flags of the item. + */ + public @Flags int getFlags() { + return mProvider.getFlags_impl(); + } + + /** + * Returns whether this item is browsable. + * @see #FLAG_BROWSABLE + */ + public boolean isBrowsable() { + return mProvider.isBrowsable_impl(); + } + + /** + * Returns whether this item is playable. + * @see #FLAG_PLAYABLE + */ + public boolean isPlayable() { + return mProvider.isPlayable_impl(); + } + + /** + * Set a metadata. If the metadata is not null, its id should be matched with this instance's + * media id. + * + * @param metadata metadata to update + */ + public void setMetadata(@Nullable MediaMetadata2 metadata) { + mProvider.setMetadata_impl(metadata); + } + + /** + * Returns the metadata of the media. + */ + public @Nullable MediaMetadata2 getMetadata() { + return mProvider.getMetadata_impl(); + } + + /** + * Returns the media id for this item. + */ + public @NonNull String getMediaId() { + return mProvider.getMediaId_impl(); + } + + /** + * Return the {@link DataSourceDesc} + * <p> + * Can be {@code null} if the MediaItem2 came from another process and anonymized + * + * @return data source descriptor + */ + public @Nullable DataSourceDesc getDataSourceDesc() { + return mProvider.getDataSourceDesc_impl(); + } +} diff --git a/media/java/android/media/MediaLibraryService2.java b/media/java/android/media/MediaLibraryService2.java new file mode 100644 index 000000000000..7a05d3c0724a --- /dev/null +++ b/media/java/android/media/MediaLibraryService2.java @@ -0,0 +1,379 @@ +/* + * Copyright 2018 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.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.content.Context; +import android.media.MediaSession2.BuilderBase; +import android.media.MediaSession2.ControllerInfo; +import android.media.update.ApiLoader; +import android.media.update.MediaLibraryService2Provider.LibraryRootProvider; +import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider; +import android.media.update.MediaSessionService2Provider; +import android.os.Bundle; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Base class for media library services. + * <p> + * Media library services enable applications to browse media content provided by an application + * and ask the application to start playing it. They may also be used to control content that + * is already playing by way of a {@link MediaSession2}. + * <p> + * To extend this class, adding followings directly to your {@code AndroidManifest.xml}. + * <pre> + * <service android:name="component_name_of_your_implementation" > + * <intent-filter> + * <action android:name="android.media.MediaLibraryService2" /> + * </intent-filter> + * </service></pre> + * <p> + * A {@link MediaLibraryService2} is extension of {@link MediaSessionService2}. IDs shouldn't + * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By + * default, an empty string will be used for ID of the service. If you want to specify an ID, + * declare metadata in the manifest as follows. + * @hide + */ +public abstract class MediaLibraryService2 extends MediaSessionService2 { + /** + * This is the interface name that a service implementing a session service should say that it + * support -- that is, this is the action it uses for its intent filter. + */ + public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2"; + + /** + * Session for the {@link MediaLibraryService2}. Build this object with + * {@link MediaLibrarySessionBuilder} and return in {@link #onCreateSession(String)}. + */ + public static class MediaLibrarySession extends MediaSession2 { + private final MediaLibrarySessionProvider mProvider; + + /** + * @hide + */ + @SystemApi + public MediaLibrarySession(MediaLibrarySessionProvider provider) { + super(provider); + mProvider = provider; + } + + /** + * Notify subscribed controller about change in a parent's children. + * + * @param controller controller to notify + * @param parentId + * @param extras + */ + public void notifyChildrenChanged(@NonNull ControllerInfo controller, + @NonNull String parentId, @NonNull Bundle extras) { + mProvider.notifyChildrenChanged_impl(controller, parentId, extras); + } + + /** + * Notify subscribed controller about change in a parent's children. + * + * @param parentId parent id + * @param extras extra bundle + */ + // This is for the backward compatibility. + public void notifyChildrenChanged(@NonNull String parentId, @Nullable Bundle extras) { + mProvider.notifyChildrenChanged_impl(parentId, extras); + } + } + + /** + * Callback for the {@link MediaLibrarySession}. + */ + public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback { + + public MediaLibrarySessionCallback(Context context) { + super(context); + } + + /** + * Called to get the root information for browsing by a particular client. + * <p> + * The implementation should verify that the client package has permission + * to access browse media information before returning the root id; it + * should return null if the client is not allowed to access this + * information. + * + * @param controllerInfo information of the controller requesting access to browse media. + * @param rootHints An optional bundle of service-specific arguments to send + * to the media library service when connecting and retrieving the + * root id for browsing, or null if none. The contents of this + * bundle may affect the information returned when browsing. + * @return The {@link LibraryRoot} for accessing this app's content or null. + * @see LibraryRoot#EXTRA_RECENT + * @see LibraryRoot#EXTRA_OFFLINE + * @see LibraryRoot#EXTRA_SUGGESTED + */ + public @Nullable LibraryRoot onGetRoot(@NonNull ControllerInfo controllerInfo, + @Nullable Bundle rootHints) { + return null; + } + + /** + * Called to get an item. Return result here for the browser. + * <p> + * Return {@code null} for no result or error. + * + * @param itemId item id to get media item. + * @return a media item. {@code null} for no result or error. + */ + public @Nullable MediaItem2 onLoadItem(@NonNull ControllerInfo controllerInfo, + @NonNull String itemId) { + return null; + } + + /** + * Called to get children of given parent id. Return the children here for the browser. + * <p> + * Return an empty list for no children, and return {@code null} for the error. + * + * @param parentId parent id to get children + * @param page number of page + * @param pageSize size of the page + * @param extras extra bundle + * @return list of children. Can be {@code null}. + */ + public @Nullable List<MediaItem2> onLoadChildren(@NonNull ControllerInfo controller, + @NonNull String parentId, int page, int pageSize, @Nullable Bundle extras) { + return null; + } + + /** + * Called when a controller subscribes to the parent. + * + * @param controller controller + * @param parentId parent id + * @param extras extra bundle + */ + public void onSubscribed(@NonNull ControllerInfo controller, String parentId, + @Nullable Bundle extras) { + } + + /** + * Called when a controller unsubscribes to the parent. + * + * @param controller controller + * @param parentId parent id + * @param extras extra bundle + */ + public void onUnsubscribed(@NonNull ControllerInfo controller, String parentId, + @Nullable Bundle extras) { + } + + /** + * Called when a controller requests search. + * + * @param query The search query sent from the media browser. It contains keywords separated + * by space. + * @param extras The bundle of service-specific arguments sent from the media browser. + */ + public void onSearch(@NonNull ControllerInfo controllerInfo, @NonNull String query, + @Nullable Bundle extras) { + + } + + /** + * Called to get the search result. Return search result here for the browser which has + * requested search previously. + * <p> + * Return an empty list for no search result, and return {@code null} for the error. + * + * @param controllerInfo Information of the controller requesting the search result. + * @param query The search query which was previously sent through + * {@link #onSearch(ControllerInfo, String, Bundle)} call. + * @param page page number. Starts from {@code 1}. + * @param pageSize page size. Should be greater or equal to {@code 1}. + * @param extras The bundle of service-specific arguments sent from the media browser. + * @return search result. {@code null} for error. + */ + public @Nullable List<MediaItem2> onLoadSearchResult(@NonNull ControllerInfo controllerInfo, + @NonNull String query, int page, int pageSize, @Nullable Bundle extras) { + return null; + } + } + + /** + * Builder for {@link MediaLibrarySession}. + */ + // Override all methods just to show them with the type instead of generics in Javadoc. + // This workarounds javadoc issue described in the MediaSession2.BuilderBase. + public class MediaLibrarySessionBuilder extends BuilderBase<MediaLibrarySession, + MediaLibrarySessionBuilder, MediaLibrarySessionCallback> { + public MediaLibrarySessionBuilder( + @NonNull Context context, @NonNull MediaPlayerInterface player, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull MediaLibrarySessionCallback callback) { + super((instance) -> ApiLoader.getProvider(context).createMediaLibraryService2Builder( + context, (MediaLibrarySessionBuilder) instance, player, callbackExecutor, + callback)); + } + + @Override + public MediaLibrarySessionBuilder setVolumeProvider( + @Nullable VolumeProvider2 volumeProvider) { + return super.setVolumeProvider(volumeProvider); + } + + @Override + public MediaLibrarySessionBuilder setRatingType(int type) { + return super.setRatingType(type); + } + + @Override + public MediaLibrarySessionBuilder setSessionActivity(@Nullable PendingIntent pi) { + return super.setSessionActivity(pi); + } + + @Override + public MediaLibrarySessionBuilder setId(String id) { + return super.setId(id); + } + + @Override + public MediaLibrarySessionBuilder setSessionCallback( + @NonNull Executor executor, @NonNull MediaLibrarySessionCallback callback) { + return super.setSessionCallback(executor, callback); + } + + @Override + public MediaLibrarySession build() { + return super.build(); + } + } + + @Override + MediaSessionService2Provider createProvider() { + return ApiLoader.getProvider(this).createMediaLibraryService2(this); + } + + /** + * Called when another app requested to start this service. + * <p> + * Library service will accept or reject the connection with the + * {@link MediaLibrarySessionCallback} in the created session. + * <p> + * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the + * expected ID that you've specified through the AndroidManifest.xml. + * <p> + * This method will be called on the main thread. + * + * @param sessionId session id written in the AndroidManifest.xml. + * @return a new library session + * @see MediaLibrarySessionBuilder + * @see #getSession() + * @throws RuntimeException if returned session is invalid + */ + @Override + public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId); + + /** + * Contains information that the library service needs to send to the client when + * {@link MediaBrowser2#getLibraryRoot(Bundle)} is called. + */ + public static final class LibraryRoot { + /** + * The lookup key for a boolean that indicates whether the library service should return a + * librar root for recently played media items. + * + * <p>When creating a media browser for a given media library service, this key can be + * supplied as a root hint for retrieving media items that are recently played. + * If the media library service can provide such media items, the implementation must return + * the key in the root hint when + * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back. + * + * <p>The root hint may contain multiple keys. + * + * @see #EXTRA_OFFLINE + * @see #EXTRA_SUGGESTED + */ + public static final String EXTRA_RECENT = "android.media.extra.RECENT"; + + /** + * The lookup key for a boolean that indicates whether the library service should return a + * library root for offline media items. + * + * <p>When creating a media browser for a given media library service, this key can be + * supplied as a root hint for retrieving media items that are can be played without an + * internet connection. + * If the media library service can provide such media items, the implementation must return + * the key in the root hint when + * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back. + * + * <p>The root hint may contain multiple keys. + * + * @see #EXTRA_RECENT + * @see #EXTRA_SUGGESTED + */ + public static final String EXTRA_OFFLINE = "android.media.extra.OFFLINE"; + + /** + * The lookup key for a boolean that indicates whether the library service should return a + * library root for suggested media items. + * + * <p>When creating a media browser for a given media library service, this key can be + * supplied as a root hint for retrieving the media items suggested by the media library + * service. The list of media items is considered ordered by relevance, first being the top + * suggestion. + * If the media library service can provide such media items, the implementation must return + * the key in the root hint when + * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back. + * + * <p>The root hint may contain multiple keys. + * + * @see #EXTRA_RECENT + * @see #EXTRA_OFFLINE + */ + public static final String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED"; + + private final LibraryRootProvider mProvider; + + /** + * Constructs a library root. + * @param rootId The root id for browsing. + * @param extras Any extras about the library service. + */ + public LibraryRoot(@NonNull Context context, + @NonNull String rootId, @Nullable Bundle extras) { + mProvider = ApiLoader.getProvider(context).createMediaLibraryService2LibraryRoot( + context, this, rootId, extras); + } + + /** + * Gets the root id for browsing. + */ + public String getRootId() { + return mProvider.getRootId_impl(); + } + + /** + * Gets any extras about the library service. + */ + public Bundle getExtras() { + return mProvider.getExtras_impl(); + } + } +} diff --git a/media/java/android/media/MediaMetadata.java b/media/java/android/media/MediaMetadata.java index 31eb948dcd09..94d4d55639dc 100644 --- a/media/java/android/media/MediaMetadata.java +++ b/media/java/android/media/MediaMetadata.java @@ -45,34 +45,61 @@ public final class MediaMetadata implements Parcelable { /** * @hide */ - @StringDef({METADATA_KEY_TITLE, METADATA_KEY_ARTIST, METADATA_KEY_ALBUM, METADATA_KEY_AUTHOR, - METADATA_KEY_WRITER, METADATA_KEY_COMPOSER, METADATA_KEY_COMPILATION, - METADATA_KEY_DATE, METADATA_KEY_GENRE, METADATA_KEY_ALBUM_ARTIST, METADATA_KEY_ART_URI, - METADATA_KEY_ALBUM_ART_URI, METADATA_KEY_DISPLAY_TITLE, METADATA_KEY_DISPLAY_SUBTITLE, - METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_KEY_DISPLAY_ICON_URI, - METADATA_KEY_MEDIA_ID, METADATA_KEY_MEDIA_URI}) + @StringDef(prefix = { "METADATA_KEY_" }, value = { + METADATA_KEY_TITLE, + METADATA_KEY_ARTIST, + METADATA_KEY_ALBUM, + METADATA_KEY_AUTHOR, + METADATA_KEY_WRITER, + METADATA_KEY_COMPOSER, + METADATA_KEY_COMPILATION, + METADATA_KEY_DATE, + METADATA_KEY_GENRE, + METADATA_KEY_ALBUM_ARTIST, + METADATA_KEY_ART_URI, + METADATA_KEY_ALBUM_ART_URI, + METADATA_KEY_DISPLAY_TITLE, + METADATA_KEY_DISPLAY_SUBTITLE, + METADATA_KEY_DISPLAY_DESCRIPTION, + METADATA_KEY_DISPLAY_ICON_URI, + METADATA_KEY_MEDIA_ID, + METADATA_KEY_MEDIA_URI, + }) @Retention(RetentionPolicy.SOURCE) public @interface TextKey {} /** * @hide */ - @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER, - METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE}) + @StringDef(prefix = { "METADATA_KEY_" }, value = { + METADATA_KEY_DURATION, + METADATA_KEY_YEAR, + METADATA_KEY_TRACK_NUMBER, + METADATA_KEY_NUM_TRACKS, + METADATA_KEY_DISC_NUMBER, + METADATA_KEY_BT_FOLDER_TYPE, + }) @Retention(RetentionPolicy.SOURCE) public @interface LongKey {} /** * @hide */ - @StringDef({METADATA_KEY_ART, METADATA_KEY_ALBUM_ART, METADATA_KEY_DISPLAY_ICON}) + @StringDef(prefix = { "METADATA_KEY_" }, value = { + METADATA_KEY_ART, + METADATA_KEY_ALBUM_ART, + METADATA_KEY_DISPLAY_ICON, + }) @Retention(RetentionPolicy.SOURCE) public @interface BitmapKey {} /** * @hide */ - @StringDef({METADATA_KEY_USER_RATING, METADATA_KEY_RATING}) + @StringDef(prefix = { "METADATA_KEY_" }, value = { + METADATA_KEY_USER_RATING, + METADATA_KEY_RATING, + }) @Retention(RetentionPolicy.SOURCE) public @interface RatingKey {} diff --git a/media/java/android/media/MediaMetadata2.java b/media/java/android/media/MediaMetadata2.java new file mode 100644 index 000000000000..54a9057eeb2d --- /dev/null +++ b/media/java/android/media/MediaMetadata2.java @@ -0,0 +1,683 @@ +/* + * Copyright 2018 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.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; +import android.annotation.SystemApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.media.update.ApiLoader; +import android.media.update.MediaMetadata2Provider; +import android.net.Uri; +import android.os.Bundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Set; + +/** + * Contains metadata about an item, such as the title, artist, etc. + * + * @hide + */ +public final class MediaMetadata2 { + // New version of MediaMetadata that no longer implements Parcelable but added from/toBundle() + // for updatable. + // MediaDescription is deprecated because it was insufficient for controller to display media + // contents. Added getExtra() here to support all the features from the MediaDescription. + + /** + * The title of the media. + */ + public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + + /** + * The artist of the media. + */ + public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + + /** + * The duration of the media in ms. A negative duration indicates that the + * duration is unknown (or infinite). + */ + public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + + /** + * The album title for the media. + */ + public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + + /** + * The author of the media. + */ + public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + + /** + * The writer of the media. + */ + public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + + /** + * The composer of the media. + */ + public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + + /** + * The compilation status of the media. + */ + public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION"; + + /** + * The date the media was created or published. The format is unspecified + * but RFC 3339 is recommended. + */ + public static final String METADATA_KEY_DATE = "android.media.metadata.DATE"; + + /** + * The year the media was created or published as a long. + */ + public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + + /** + * The genre of the media. + */ + public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + + /** + * The track number for the media. + */ + public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + + /** + * The number of tracks in the media's original source. + */ + public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS"; + + /** + * The disc number for the media's original source. + */ + public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + + /** + * The artist for the album of the media's original source. + */ + public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + + /** + * The artwork for the media as a {@link Bitmap}. + * + * The artwork should be relatively small and may be scaled down + * if it is too large. For higher resolution artwork + * {@link #METADATA_KEY_ART_URI} should be used instead. + */ + public static final String METADATA_KEY_ART = "android.media.metadata.ART"; + + /** + * The artwork for the media as a Uri style String. + */ + public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI"; + + /** + * The artwork for the album of the media's original source as a + * {@link Bitmap}. + * The artwork should be relatively small and may be scaled down + * if it is too large. For higher resolution artwork + * {@link #METADATA_KEY_ALBUM_ART_URI} should be used instead. + */ + public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + + /** + * The artwork for the album of the media's original source as a Uri style + * String. + */ + public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI"; + + /** + * The user's rating for the media. + * + * @see Rating + */ + public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + + /** + * The overall rating for the media. + * + * @see Rating + */ + public static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + + /** + * A title that is suitable for display to the user. This will generally be + * the same as {@link #METADATA_KEY_TITLE} but may differ for some formats. + * When displaying media described by this metadata this should be preferred + * if present. + */ + public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE"; + + /** + * A subtitle that is suitable for display to the user. When displaying a + * second line for media described by this metadata this should be preferred + * to other fields if present. + */ + public static final String METADATA_KEY_DISPLAY_SUBTITLE + = "android.media.metadata.DISPLAY_SUBTITLE"; + + /** + * A description that is suitable for display to the user. When displaying + * more information for media described by this metadata this should be + * preferred to other fields if present. + */ + public static final String METADATA_KEY_DISPLAY_DESCRIPTION + = "android.media.metadata.DISPLAY_DESCRIPTION"; + + /** + * An icon or thumbnail that is suitable for display to the user. When + * displaying an icon for media described by this metadata this should be + * preferred to other fields if present. This must be a {@link Bitmap}. + * + * The icon should be relatively small and may be scaled down + * if it is too large. For higher resolution artwork + * {@link #METADATA_KEY_DISPLAY_ICON_URI} should be used instead. + */ + public static final String METADATA_KEY_DISPLAY_ICON + = "android.media.metadata.DISPLAY_ICON"; + + /** + * An icon or thumbnail that is suitable for display to the user. When + * displaying more information for media described by this metadata the + * display description should be preferred to other fields when present. + * This must be a Uri style String. + */ + public static final String METADATA_KEY_DISPLAY_ICON_URI + = "android.media.metadata.DISPLAY_ICON_URI"; + + /** + * A String key for identifying the content. This value is specific to the + * service providing the content. If used, this should be a persistent + * unique key for the underlying content. It may be used with + * {@link MediaController2#playFromMediaId(String, Bundle)} + * to initiate playback when provided by a {@link MediaBrowser2} connected to + * the same app. + */ + public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID"; + + /** + * A Uri formatted String representing the content. This value is specific to the + * service providing the content. It may be used with + * {@link MediaController2#playFromUri(String, Bundle)} + * to initiate playback when provided by a {@link MediaBrowser2} connected to + * the same app. + */ + public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI"; + + /** + * The bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth + * AVRCP 1.5. It should be one of the following: + * <ul> + * <li>{@link #BT_FOLDER_TYPE_MIXED}</li> + * <li>{@link #BT_FOLDER_TYPE_TITLES}</li> + * <li>{@link #BT_FOLDER_TYPE_ALBUMS}</li> + * <li>{@link #BT_FOLDER_TYPE_ARTISTS}</li> + * <li>{@link #BT_FOLDER_TYPE_GENRES}</li> + * <li>{@link #BT_FOLDER_TYPE_PLAYLISTS}</li> + * <li>{@link #BT_FOLDER_TYPE_YEARS}</li> + * </ul> + */ + public static final String METADATA_KEY_BT_FOLDER_TYPE + = "android.media.metadata.BT_FOLDER_TYPE"; + + /** + * The type of folder that is unknown or contains media elements of mixed types as specified in + * the section 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_MIXED = 0; + + /** + * The type of folder that contains media elements only as specified in the section 6.10.2.2 of + * the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_TITLES = 1; + + /** + * The type of folder that contains folders categorized by album as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_ALBUMS = 2; + + /** + * The type of folder that contains folders categorized by artist as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_ARTISTS = 3; + + /** + * The type of folder that contains folders categorized by genre as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_GENRES = 4; + + /** + * The type of folder that contains folders categorized by playlist as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_PLAYLISTS = 5; + + /** + * The type of folder that contains folders categorized by year as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_YEARS = 6; + + /** + * Whether the media is an advertisement. A value of 0 indicates it is not an advertisement. A + * value of 1 or non-zero indicates it is an advertisement. If not specified, this value is set + * to 0 by default. + */ + public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT"; + + /** + * The download status of the media which will be used for later offline playback. It should be + * one of the following: + * + * <ul> + * <li>{@link #STATUS_NOT_DOWNLOADED}</li> + * <li>{@link #STATUS_DOWNLOADING}</li> + * <li>{@link #STATUS_DOWNLOADED}</li> + * </ul> + */ + public static final String METADATA_KEY_DOWNLOAD_STATUS = + "android.media.metadata.DOWNLOAD_STATUS"; + + /** + * The status value to indicate the media item is not downloaded. + * + * @see #METADATA_KEY_DOWNLOAD_STATUS + */ + public static final long STATUS_NOT_DOWNLOADED = 0; + + /** + * The status value to indicate the media item is being downloaded. + * + * @see #METADATA_KEY_DOWNLOAD_STATUS + */ + public static final long STATUS_DOWNLOADING = 1; + + /** + * The status value to indicate the media item is downloaded for later offline playback. + * + * @see #METADATA_KEY_DOWNLOAD_STATUS + */ + public static final long STATUS_DOWNLOADED = 2; + + /** + * A {@link Bundle} extra. + * @hide + */ + public static final String METADATA_KEY_EXTRA = "android.media.metadata.EXTRA"; + + /** + * @hide + */ + @StringDef({METADATA_KEY_TITLE, METADATA_KEY_ARTIST, METADATA_KEY_ALBUM, METADATA_KEY_AUTHOR, + METADATA_KEY_WRITER, METADATA_KEY_COMPOSER, METADATA_KEY_COMPILATION, + METADATA_KEY_DATE, METADATA_KEY_GENRE, METADATA_KEY_ALBUM_ARTIST, METADATA_KEY_ART_URI, + METADATA_KEY_ALBUM_ART_URI, METADATA_KEY_DISPLAY_TITLE, METADATA_KEY_DISPLAY_SUBTITLE, + METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_KEY_DISPLAY_ICON_URI, + METADATA_KEY_MEDIA_ID, METADATA_KEY_MEDIA_URI}) + @Retention(RetentionPolicy.SOURCE) + public @interface TextKey {} + + /** + * @hide + */ + @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER, + METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE, + METADATA_KEY_ADVERTISEMENT, METADATA_KEY_DOWNLOAD_STATUS}) + @Retention(RetentionPolicy.SOURCE) + public @interface LongKey {} + + /** + * @hide + */ + @StringDef({METADATA_KEY_ART, METADATA_KEY_ALBUM_ART, METADATA_KEY_DISPLAY_ICON}) + @Retention(RetentionPolicy.SOURCE) + public @interface BitmapKey {} + + /** + * @hide + */ + @StringDef({METADATA_KEY_USER_RATING, METADATA_KEY_RATING}) + @Retention(RetentionPolicy.SOURCE) + public @interface RatingKey {} + + private final MediaMetadata2Provider mProvider; + + /** + * @hide + */ + @SystemApi + public MediaMetadata2(MediaMetadata2Provider provider) { + mProvider = provider; + } + + /** + * Returns true if the given key is contained in the metadata + * + * @param key a String key + * @return true if the key exists in this metadata, false otherwise + */ + public boolean containsKey(@NonNull String key) { + return mProvider.containsKey_impl(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @param key The key the value is stored under + * @return a CharSequence value, or null + */ + public @Nullable CharSequence getText(@TextKey String key) { + return mProvider.getText_impl(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @return media id. Can be {@code null} + * @see #METADATA_KEY_MEDIA_ID + */ + public @Nullable String getMediaId() { + return mProvider.getMediaId_impl(); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @param key The key the value is stored under + * @return a String value, or null + */ + public @Nullable String getString(@NonNull @TextKey String key) { + return mProvider.getString_impl(key); + } + + /** + * Returns the value associated with the given key, or 0L if no long exists + * for the given key. + * + * @param key The key the value is stored under + * @return a long value + */ + public long getLong(@NonNull @LongKey String key) { + return mProvider.getLong_impl(key); + } + + /** + * Return a {@link Rating2} for the given key or null if no rating exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link Rating2} or {@code null} + */ + public @Nullable Rating2 getRating(@RatingKey String key) { + return mProvider.getRating_impl(key); + } + + /** + * Return a {@link Bitmap} for the given key or null if no bitmap exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link Bitmap} or null + */ + public Bitmap getBitmap(@BitmapKey String key) { + return mProvider.getBitmap_impl(key); + } + + /** + * Get the extra {@link Bundle} from the metadata object. + * + * @return A {@link Bundle} or {@code null} + */ + public @Nullable Bundle getExtra() { + return mProvider.getExtra_impl(); + } + + /** + * Get the number of fields in this metadata. + * + * @return The number of fields in the metadata. + */ + public int size() { + return mProvider.size_impl(); + } + + /** + * Returns a Set containing the Strings used as keys in this metadata. + * + * @return a Set of String keys + */ + public @NonNull Set<String> keySet() { + return mProvider.keySet_impl(); + } + + /** + * Gets the bundle backing the metadata object. This is available to support + * backwards compatibility. Apps should not modify the bundle directly. + * + * @return The Bundle backing this metadata. + */ + public @NonNull Bundle toBundle() { + return mProvider.toBundle_impl(); + } + + /** + * Creates the {@link MediaMetadata2} from the bundle that previously returned by + * {@link #toBundle()}. + * + * @param context context + * @param bundle bundle for the metadata + * @return a new MediaMetadata2 + */ + public static @NonNull MediaMetadata2 fromBundle(@NonNull Context context, + @Nullable Bundle bundle) { + return ApiLoader.getProvider(context).fromBundle_MediaMetadata2(context, bundle); + } + + /** + * Use to build MediaMetadata2 objects. The system defined metadata keys must + * use the appropriate data type. + */ + public static final class Builder { + private final MediaMetadata2Provider.BuilderProvider mProvider; + + /** + * Create an empty Builder. Any field that should be included in the + * {@link MediaMetadata2} must be added. + */ + public Builder(@NonNull Context context) { + mProvider = ApiLoader.getProvider(context).createMediaMetadata2Builder( + context, this); + } + + /** + * Create a Builder using a {@link MediaMetadata2} instance to set the + * initial values. All fields in the source metadata will be included in + * the new metadata. Fields can be overwritten by adding the same key. + * + * @param source + */ + public Builder(@NonNull Context context, @NonNull MediaMetadata2 source) { + mProvider = ApiLoader.getProvider(context).createMediaMetadata2Builder( + context, this, source); + } + + /** + * @hide + */ + @SystemApi + public Builder(@NonNull MediaMetadata2Provider.BuilderProvider provider) { + mProvider = provider; + } + + /** + * Put a CharSequence value into the metadata. Custom keys may be used, + * but if the METADATA_KEYs defined in this class are used they may only + * be one of the following: + * <ul> + * <li>{@link #METADATA_KEY_TITLE}</li> + * <li>{@link #METADATA_KEY_ARTIST}</li> + * <li>{@link #METADATA_KEY_ALBUM}</li> + * <li>{@link #METADATA_KEY_AUTHOR}</li> + * <li>{@link #METADATA_KEY_WRITER}</li> + * <li>{@link #METADATA_KEY_COMPOSER}</li> + * <li>{@link #METADATA_KEY_DATE}</li> + * <li>{@link #METADATA_KEY_GENRE}</li> + * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li> + * <li>{@link #METADATA_KEY_ART_URI}</li> + * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li> + * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li> + * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li> + * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li> + * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The CharSequence value to store + * @return The Builder to allow chaining + */ + public @NonNull Builder putText(@TextKey String key, @Nullable CharSequence value) { + return mProvider.putText_impl(key, value); + } + + /** + * Put a String value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_TITLE}</li> + * <li>{@link #METADATA_KEY_ARTIST}</li> + * <li>{@link #METADATA_KEY_ALBUM}</li> + * <li>{@link #METADATA_KEY_AUTHOR}</li> + * <li>{@link #METADATA_KEY_WRITER}</li> + * <li>{@link #METADATA_KEY_COMPOSER}</li> + * <li>{@link #METADATA_KEY_DATE}</li> + * <li>{@link #METADATA_KEY_GENRE}</li> + * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li> + * <li>{@link #METADATA_KEY_ART_URI}</li> + * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li> + * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li> + * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li> + * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li> + * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public @NonNull Builder putString(@TextKey String key, @Nullable String value) { + return mProvider.putString_impl(key, value); + } + + /** + * Put a long value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_DURATION}</li> + * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li> + * <li>{@link #METADATA_KEY_NUM_TRACKS}</li> + * <li>{@link #METADATA_KEY_DISC_NUMBER}</li> + * <li>{@link #METADATA_KEY_YEAR}</li> + * <li>{@link #METADATA_KEY_BT_FOLDER_TYPE}</li> + * <li>{@link #METADATA_KEY_ADVERTISEMENT}</li> + * <li>{@link #METADATA_KEY_DOWNLOAD_STATUS}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public @NonNull Builder putLong(@NonNull @LongKey String key, long value) { + return mProvider.putLong_impl(key, value); + } + + /** + * Put a {@link Rating2} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + * <ul> + * <li>{@link #METADATA_KEY_RATING}</li> + * <li>{@link #METADATA_KEY_USER_RATING}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public @NonNull Builder putRating(@NonNull @RatingKey String key, @Nullable Rating2 value) { + return mProvider.putRating_impl(key, value); + } + + /** + * Put a {@link Bitmap} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + * <ul> + * <li>{@link #METADATA_KEY_ART}</li> + * <li>{@link #METADATA_KEY_ALBUM_ART}</li> + * <li>{@link #METADATA_KEY_DISPLAY_ICON}</li> + * </ul> + * Large bitmaps may be scaled down by the system when + * {@link android.media.session.MediaSession#setMetadata} is called. + * To pass full resolution images {@link Uri Uris} should be used with + * {@link #putString}. + * + * @param key The key for referencing this value + * @param value The Bitmap to store + * @return The Builder to allow chaining + */ + public @NonNull Builder putBitmap(@NonNull @BitmapKey String key, @Nullable Bitmap value) { + return mProvider.putBitmap_impl(key, value); + } + + /** + * Set an extra {@link Bundle} into the metadata. + */ + public @NonNull Builder setExtra(@Nullable Bundle bundle) { + return mProvider.setExtra_impl(bundle); + } + + /** + * Creates a {@link MediaMetadata2} instance with the specified fields. + * + * @return The new MediaMetadata2 instance + */ + public @NonNull MediaMetadata2 build() { + return mProvider.build_impl(); + } + } +} + diff --git a/media/java/android/media/MediaMetadataRetriever.java b/media/java/android/media/MediaMetadataRetriever.java index 571b41b3673c..745eb74d6e20 100644 --- a/media/java/android/media/MediaMetadataRetriever.java +++ b/media/java/android/media/MediaMetadataRetriever.java @@ -47,7 +47,7 @@ public class MediaMetadataRetriever // The field below is accessed by native methods @SuppressWarnings("unused") private long mNativeContext; - + private static final int EMBEDDED_PICTURE_TYPE_ANY = 0xFFFF; public MediaMetadataRetriever() { @@ -58,7 +58,7 @@ public class MediaMetadataRetriever * Sets the data source (file pathname) to use. Call this * method before the rest of the methods in this class. This method may be * time-consuming. - * + * * @param path The path of the input media file. * @throws IllegalArgumentException If the path is invalid. */ @@ -113,7 +113,7 @@ public class MediaMetadataRetriever * responsibility to close the file descriptor. It is safe to do so as soon * as this call returns. Call this method before the rest of the methods in * this class. This method may be time-consuming. - * + * * @param fd the FileDescriptor for the file you want to play * @param offset the offset into the file where the data to be played starts, * in bytes. It must be non-negative @@ -123,13 +123,13 @@ public class MediaMetadataRetriever */ public native void setDataSource(FileDescriptor fd, long offset, long length) throws IllegalArgumentException; - + /** * Sets the data source (FileDescriptor) to use. It is the caller's * responsibility to close the file descriptor. It is safe to do so as soon * as this call returns. Call this method before the rest of the methods in * this class. This method may be time-consuming. - * + * * @param fd the FileDescriptor for the file you want to play * @throws IllegalArgumentException if the FileDescriptor is invalid */ @@ -138,11 +138,11 @@ public class MediaMetadataRetriever // intentionally less than LONG_MAX setDataSource(fd, 0, 0x7ffffffffffffffL); } - + /** - * Sets the data source as a content Uri. Call this method before + * Sets the data source as a content Uri. Call this method before * the rest of the methods in this class. This method may be time-consuming. - * + * * @param context the Context to use when resolving the Uri * @param uri the Content URI of the data you want to play * @throws IllegalArgumentException if the Uri is invalid @@ -154,7 +154,7 @@ public class MediaMetadataRetriever if (uri == null) { throw new IllegalArgumentException(); } - + String scheme = uri.getScheme(); if(scheme == null || scheme.equals("file")) { setDataSource(uri.getPath()); @@ -213,12 +213,12 @@ public class MediaMetadataRetriever /** * Call this method after setDataSource(). This method retrieves the * meta data value associated with the keyCode. - * + * * The keyCode currently supported is listed below as METADATA_XXX * constants. With any other value, it returns a null pointer. - * + * * @param keyCode One of the constants listed below at the end of the class. - * @return The meta data value associate with the given keyCode on success; + * @return The meta data value associate with the given keyCode on success; * null on failure. */ public native String extractMetadata(int keyCode); @@ -368,6 +368,109 @@ public class MediaMetadataRetriever private native Bitmap _getFrameAtTime(long timeUs, int option, int width, int height); /** + * This method retrieves a video frame by its index. It should only be called + * after {@link #setDataSource}. + * + * @param frameIndex 0-based index of the video frame. The frame index must be that of + * a valid frame. The total number of frames available for retrieval can be queried + * via the {@link #METADATA_KEY_VIDEO_FRAME_COUNT} key. + * + * @throws IllegalStateException if the container doesn't contain video or image sequences. + * @throws IllegalArgumentException if the requested frame index does not exist. + * + * @return A Bitmap containing the requested video frame, or null if the retrieval fails. + * + * @see #getFramesAtIndex(int, int) + */ + public Bitmap getFrameAtIndex(int frameIndex) { + Bitmap[] bitmaps = getFramesAtIndex(frameIndex, 1); + if (bitmaps == null || bitmaps.length < 1) { + return null; + } + return bitmaps[0]; + } + + /** + * This method retrieves a consecutive set of video frames starting at the + * specified index. It should only be called after {@link #setDataSource}. + * + * If the caller intends to retrieve more than one consecutive video frames, + * this method is preferred over {@link #getFrameAtIndex(int)} for efficiency. + * + * @param frameIndex 0-based index of the first video frame to retrieve. The frame index + * must be that of a valid frame. The total number of frames available for retrieval + * can be queried via the {@link #METADATA_KEY_VIDEO_FRAME_COUNT} key. + * @param numFrames number of consecutive video frames to retrieve. Must be a positive + * value. The stream must contain at least numFrames frames starting at frameIndex. + * + * @throws IllegalStateException if the container doesn't contain video or image sequences. + * @throws IllegalArgumentException if the frameIndex or numFrames is invalid, or the + * stream doesn't contain at least numFrames starting at frameIndex. + + * @return An array of Bitmaps containing the requested video frames. The returned + * array could contain less frames than requested if the retrieval fails. + * + * @see #getFrameAtIndex(int) + */ + public Bitmap[] getFramesAtIndex(int frameIndex, int numFrames) { + if (!"yes".equals(extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO))) { + throw new IllegalStateException("Does not contail video or image sequences"); + } + int frameCount = Integer.parseInt( + extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT)); + if (frameIndex < 0 || numFrames < 1 + || frameIndex >= frameCount + || frameIndex > frameCount - numFrames) { + throw new IllegalArgumentException("Invalid frameIndex or numFrames: " + + frameIndex + ", " + numFrames); + } + return _getFrameAtIndex(frameIndex, numFrames); + } + private native Bitmap[] _getFrameAtIndex(int frameIndex, int numFrames); + + /** + * This method retrieves a still image by its index. It should only be called + * after {@link #setDataSource}. + * + * @param imageIndex 0-based index of the image, with negative value indicating + * the primary image. + * @throws IllegalStateException if the container doesn't contain still images. + * @throws IllegalArgumentException if the requested image does not exist. + * + * @return the requested still image, or null if the image cannot be retrieved. + * + * @see #getPrimaryImage + */ + public Bitmap getImageAtIndex(int imageIndex) { + if (!"yes".equals(extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE))) { + throw new IllegalStateException("Does not contail still images"); + } + + String imageCount = extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT); + if (imageIndex >= Integer.parseInt(imageCount)) { + throw new IllegalArgumentException("Invalid image index: " + imageCount); + } + + return _getImageAtIndex(imageIndex); + } + + /** + * This method retrieves the primary image of the media content. It should only + * be called after {@link #setDataSource}. + * + * @return the primary image, or null if it cannot be retrieved. + * + * @throws IllegalStateException if the container doesn't contain still images. + * + * @see #getImageAtIndex(int) + */ + public Bitmap getPrimaryImage() { + return getImageAtIndex(-1); + } + + private native Bitmap _getImageAtIndex(int imageIndex); + + /** * Call this method after setDataSource(). This method finds the optional * graphic or album/cover art associated associated with the data source. If * there are more than one pictures, (any) one of them is returned. @@ -406,7 +509,7 @@ public class MediaMetadataRetriever * @see #getFrameAtTime(long, int) */ /* Do not change these option values without updating their counterparts - * in include/media/stagefright/MediaSource.h! + * in include/media/MediaSource.h! */ /** * This option is used with {@link #getFrameAtTime(long, int)} to retrieve @@ -583,5 +686,40 @@ public class MediaMetadataRetriever * number. */ public static final int METADATA_KEY_CAPTURE_FRAMERATE = 25; + /** + * If this key exists the media contains still image content. + */ + public static final int METADATA_KEY_HAS_IMAGE = 26; + /** + * If the media contains still images, this key retrieves the number + * of still images. + */ + public static final int METADATA_KEY_IMAGE_COUNT = 27; + /** + * If the media contains still images, this key retrieves the image + * index of the primary image. + */ + public static final int METADATA_KEY_IMAGE_PRIMARY = 28; + /** + * If the media contains still images, this key retrieves the width + * of the primary image. + */ + public static final int METADATA_KEY_IMAGE_WIDTH = 29; + /** + * If the media contains still images, this key retrieves the height + * of the primary image. + */ + public static final int METADATA_KEY_IMAGE_HEIGHT = 30; + /** + * If the media contains still images, this key retrieves the rotation + * of the primary image. + */ + public static final int METADATA_KEY_IMAGE_ROTATION = 31; + /** + * If the media contains video and this key exists, it retrieves the + * total number of frames in the video sequence. + */ + public static final int METADATA_KEY_VIDEO_FRAME_COUNT = 32; + // Add more here... } diff --git a/media/java/android/media/MediaMuxer.java b/media/java/android/media/MediaMuxer.java index 91e57ee073b0..02c71b283b21 100644 --- a/media/java/android/media/MediaMuxer.java +++ b/media/java/android/media/MediaMuxer.java @@ -258,12 +258,18 @@ final public class MediaMuxer { * in include/media/stagefright/MediaMuxer.h! */ private OutputFormat() {} + /** @hide */ + public static final int MUXER_OUTPUT_FIRST = 0; /** MPEG4 media file format*/ - public static final int MUXER_OUTPUT_MPEG_4 = 0; + public static final int MUXER_OUTPUT_MPEG_4 = MUXER_OUTPUT_FIRST; /** WEBM media file format*/ - public static final int MUXER_OUTPUT_WEBM = 1; + public static final int MUXER_OUTPUT_WEBM = MUXER_OUTPUT_FIRST + 1; /** 3GPP media file format*/ - public static final int MUXER_OUTPUT_3GPP = 2; + public static final int MUXER_OUTPUT_3GPP = MUXER_OUTPUT_FIRST + 2; + /** HEIF media file format*/ + public static final int MUXER_OUTPUT_HEIF = MUXER_OUTPUT_FIRST + 3; + /** @hide */ + public static final int MUXER_OUTPUT_LAST = MUXER_OUTPUT_HEIF; }; /** @hide */ @@ -271,6 +277,7 @@ final public class MediaMuxer { OutputFormat.MUXER_OUTPUT_MPEG_4, OutputFormat.MUXER_OUTPUT_WEBM, OutputFormat.MUXER_OUTPUT_3GPP, + OutputFormat.MUXER_OUTPUT_HEIF, }) @Retention(RetentionPolicy.SOURCE) public @interface Format {} @@ -347,8 +354,7 @@ final public class MediaMuxer { } private void setUpMediaMuxer(@NonNull FileDescriptor fd, @Format int format) throws IOException { - if (format != OutputFormat.MUXER_OUTPUT_MPEG_4 && format != OutputFormat.MUXER_OUTPUT_WEBM - && format != OutputFormat.MUXER_OUTPUT_3GPP) { + if (format < OutputFormat.MUXER_OUTPUT_FIRST || format > OutputFormat.MUXER_OUTPUT_LAST) { throw new IllegalArgumentException("format: " + format + " is invalid"); } mNativeObject = nativeSetup(fd, format); diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index 31ffc4b59ea8..fe5e8226e159 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -43,6 +43,7 @@ import android.system.Os; import android.system.OsConstants; import android.util.Log; import android.util.Pair; +import android.util.ArrayMap; import android.view.Surface; import android.view.SurfaceHolder; import android.widget.VideoView; @@ -58,6 +59,7 @@ import android.media.SubtitleData; import android.media.SubtitleTrack.RenderingWidget; import android.media.SyncParams; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import libcore.io.IoBridge; @@ -577,6 +579,7 @@ import java.util.Vector; public class MediaPlayer extends PlayerBase implements SubtitleController.Listener , VolumeAutomation + , AudioRouting { /** Constant to retrieve only the new metadata since the last @@ -1417,6 +1420,127 @@ public class MediaPlayer extends PlayerBase private native @Nullable VolumeShaper.State native_getVolumeShaperState(int id); + //-------------------------------------------------------------------------- + // Explicit Routing + //-------------------- + private AudioDeviceInfo mPreferredDevice = null; + + /** + * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route + * the output from this MediaPlayer. + * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source. + * If deviceInfo is null, default routing is restored. + * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and + * does not correspond to a valid audio device. + */ + @Override + public boolean setPreferredDevice(AudioDeviceInfo deviceInfo) { + if (deviceInfo != null && !deviceInfo.isSink()) { + return false; + } + int preferredDeviceId = deviceInfo != null ? deviceInfo.getId() : 0; + boolean status = native_setOutputDevice(preferredDeviceId); + if (status == true) { + synchronized (this) { + mPreferredDevice = deviceInfo; + } + } + return status; + } + + /** + * Returns the selected output specified by {@link #setPreferredDevice}. Note that this + * is not guaranteed to correspond to the actual device being used for playback. + */ + @Override + public AudioDeviceInfo getPreferredDevice() { + synchronized (this) { + return mPreferredDevice; + } + } + + /** + * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer + * Note: The query is only valid if the MediaPlayer is currently playing. + * If the player is not playing, the returned device can be null or correspond to previously + * selected device when the player was last active. + */ + @Override + public AudioDeviceInfo getRoutedDevice() { + int deviceId = native_getRoutedDeviceId(); + if (deviceId == 0) { + return null; + } + AudioDeviceInfo[] devices = + AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS); + for (int i = 0; i < devices.length; i++) { + if (devices[i].getId() == deviceId) { + return devices[i]; + } + } + return null; + } + + /* + * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler. + */ + @GuardedBy("mRoutingChangeListeners") + private void enableNativeRoutingCallbacksLocked(boolean enabled) { + if (mRoutingChangeListeners.size() == 0) { + native_enableDeviceCallback(enabled); + } + } + + /** + * The list of AudioRouting.OnRoutingChangedListener interfaces added (with + * {@link #addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, Handler)} + * by an app to receive (re)routing notifications. + */ + @GuardedBy("mRoutingChangeListeners") + private ArrayMap<AudioRouting.OnRoutingChangedListener, + NativeRoutingEventHandlerDelegate> mRoutingChangeListeners = new ArrayMap<>(); + + /** + * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing + * changes on this MediaPlayer. + * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive + * notifications of rerouting events. + * @param handler Specifies the {@link Handler} object for the thread on which to execute + * the callback. If <code>null</code>, the handler on the main looper will be used. + */ + @Override + public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener, + Handler handler) { + synchronized (mRoutingChangeListeners) { + if (listener != null && !mRoutingChangeListeners.containsKey(listener)) { + enableNativeRoutingCallbacksLocked(true); + mRoutingChangeListeners.put( + listener, new NativeRoutingEventHandlerDelegate(this, listener, + handler != null ? handler : mEventHandler)); + } + } + } + + /** + * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added + * to receive rerouting notifications. + * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface + * to remove. + */ + @Override + public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener) { + synchronized (mRoutingChangeListeners) { + if (mRoutingChangeListeners.containsKey(listener)) { + mRoutingChangeListeners.remove(listener); + enableNativeRoutingCallbacksLocked(false); + } + } + } + + private native final boolean native_setOutputDevice(int deviceId); + private native final int native_getRoutedDeviceId(); + private native final void native_enableDeviceCallback(boolean enabled); + /** * Set the low-level power management behavior for this MediaPlayer. This * can be used when the MediaPlayer is not playing through a SurfaceHolder @@ -1546,21 +1670,9 @@ public class MediaPlayer extends PlayerBase public native boolean isPlaying(); /** - * Gets the default buffering management params. - * Calling it only after {@code setDataSource} has been called. - * Each type of data source might have different set of default params. - * - * @return the default buffering management params supported by the source component. - * @throws IllegalStateException if the internal player engine has not been - * initialized, or {@code setDataSource} has not been called. - * @hide - */ - @NonNull - public native BufferingParams getDefaultBufferingParams(); - - /** * Gets the current buffering management params used by the source component. * Calling it only after {@code setDataSource} has been called. + * Each type of data source might have different set of default params. * * @return the current buffering management params used by the source component. * @throws IllegalStateException if the internal player engine has not been @@ -1575,8 +1687,7 @@ public class MediaPlayer extends PlayerBase * The object sets its internal BufferingParams to the input, except that the input is * invalid or not supported. * Call it only after {@code setDataSource} has been called. - * Users should only use supported mode returned by {@link #getDefaultBufferingParams()} - * or its downsized version as described in {@link BufferingParams}. + * The input is a hint to MediaPlayer. * * @param params the buffering management params. * @@ -2072,6 +2183,20 @@ public class MediaPlayer extends PlayerBase private native void _reset(); /** + * Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be + * notified when the presentation time reaches (becomes greater than or equal to) + * the value specified. + * + * @param mediaTimeUs presentation time to get timed event callback at + * @hide + */ + public void notifyAt(long mediaTimeUs) { + _notifyAt(mediaTimeUs); + } + + private native void _notifyAt(long mediaTimeUs); + + /** * Sets the audio stream type for this MediaPlayer. See {@link AudioManager} * for a list of stream types. Must call this method before prepare() or * prepareAsync() in order for the target stream type to become effective @@ -3155,12 +3280,14 @@ public class MediaPlayer extends PlayerBase private static final int MEDIA_PAUSED = 7; private static final int MEDIA_STOPPED = 8; private static final int MEDIA_SKIPPED = 9; + private static final int MEDIA_NOTIFY_TIME = 98; private static final int MEDIA_TIMED_TEXT = 99; private static final int MEDIA_ERROR = 100; private static final int MEDIA_INFO = 200; private static final int MEDIA_SUBTITLE_DATA = 201; private static final int MEDIA_META_DATA = 202; private static final int MEDIA_DRM_INFO = 210; + private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000; private TimeProvider mTimeProvider; @@ -3345,6 +3472,14 @@ public class MediaPlayer extends PlayerBase } // No real default action so far. return; + + case MEDIA_NOTIFY_TIME: + TimeProvider timeProvider = mTimeProvider; + if (timeProvider != null) { + timeProvider.onNotifyTime(); + } + return; + case MEDIA_TIMED_TEXT: OnTimedTextListener onTimedTextListener = mOnTimedTextListener; if (onTimedTextListener == null) @@ -3391,6 +3526,16 @@ public class MediaPlayer extends PlayerBase case MEDIA_NOP: // interface test message - ignore break; + case MEDIA_AUDIO_ROUTING_CHANGED: + AudioManager.resetAudioPortGeneration(); + synchronized (mRoutingChangeListeners) { + for (NativeRoutingEventHandlerDelegate delegate + : mRoutingChangeListeners.values()) { + delegate.notifyClient(); + } + } + return; + default: Log.e(TAG, "Unknown message type " + msg.what); return; @@ -5144,19 +5289,16 @@ public class MediaPlayer extends PlayerBase private boolean mStopped = true; private boolean mBuffering; private long mLastReportedTime; - private long mTimeAdjustment; // since we are expecting only a handful listeners per stream, there is // no need for log(N) search performance private MediaTimeProvider.OnMediaTimeListener mListeners[]; private long mTimes[]; - private long mLastNanoTime; private Handler mEventHandler; private boolean mRefresh = false; private boolean mPausing = false; private boolean mSeeking = false; private static final int NOTIFY = 1; private static final int NOTIFY_TIME = 0; - private static final int REFRESH_AND_NOTIFY_TIME = 1; private static final int NOTIFY_STOP = 2; private static final int NOTIFY_SEEK = 3; private static final int NOTIFY_TRACK_DATA = 4; @@ -5188,13 +5330,11 @@ public class MediaPlayer extends PlayerBase mListeners = new MediaTimeProvider.OnMediaTimeListener[0]; mTimes = new long[0]; mLastTimeUs = 0; - mTimeAdjustment = 0; } private void scheduleNotification(int type, long delayUs) { // ignore time notifications until seek is handled - if (mSeeking && - (type == NOTIFY_TIME || type == REFRESH_AND_NOTIFY_TIME)) { + if (mSeeking && type == NOTIFY_TIME) { return; } @@ -5221,6 +5361,14 @@ public class MediaPlayer extends PlayerBase } /** @hide */ + public void onNotifyTime() { + synchronized (this) { + if (DEBUG) Log.d(TAG, "onNotifyTime: "); + scheduleNotification(NOTIFY_TIME, 0 /* delay */); + } + } + + /** @hide */ public void onPaused(boolean paused) { synchronized(this) { if (DEBUG) Log.d(TAG, "onPaused: " + paused); @@ -5231,7 +5379,7 @@ public class MediaPlayer extends PlayerBase } else { mPausing = paused; // special handling if player disappeared mSeeking = false; - scheduleNotification(REFRESH_AND_NOTIFY_TIME, 0 /* delay */); + scheduleNotification(NOTIFY_TIME, 0 /* delay */); } } } @@ -5241,7 +5389,7 @@ public class MediaPlayer extends PlayerBase synchronized (this) { if (DEBUG) Log.d(TAG, "onBuffering: " + buffering); mBuffering = buffering; - scheduleNotification(REFRESH_AND_NOTIFY_TIME, 0 /* delay */); + scheduleNotification(NOTIFY_TIME, 0 /* delay */); } } @@ -5438,7 +5586,7 @@ public class MediaPlayer extends PlayerBase if (nextTimeUs > nowUs && !mPaused) { // schedule callback at nextTimeUs if (DEBUG) Log.d(TAG, "scheduling for " + nextTimeUs + " and " + nowUs); - scheduleNotification(NOTIFY_TIME, nextTimeUs - nowUs); + mPlayer.notifyAt(nextTimeUs); } else { mEventHandler.removeMessages(NOTIFY); // no more callbacks @@ -5449,25 +5597,6 @@ public class MediaPlayer extends PlayerBase } } - private long getEstimatedTime(long nanoTime, boolean monotonic) { - if (mPaused) { - mLastReportedTime = mLastTimeUs + mTimeAdjustment; - } else { - long timeSinceRead = (nanoTime - mLastNanoTime) / 1000; - mLastReportedTime = mLastTimeUs + timeSinceRead; - if (mTimeAdjustment > 0) { - long adjustment = - mTimeAdjustment - timeSinceRead / TIME_ADJUSTMENT_RATE; - if (adjustment <= 0) { - mTimeAdjustment = 0; - } else { - mLastReportedTime += adjustment; - } - } - } - return mLastReportedTime; - } - public long getCurrentTimeUs(boolean refreshTime, boolean monotonic) throws IllegalStateException { synchronized (this) { @@ -5477,42 +5606,38 @@ public class MediaPlayer extends PlayerBase return mLastReportedTime; } - long nanoTime = System.nanoTime(); - if (refreshTime || - nanoTime >= mLastNanoTime + MAX_NS_WITHOUT_POSITION_CHECK) { - try { - mLastTimeUs = mPlayer.getCurrentPosition() * 1000L; - mPaused = !mPlayer.isPlaying() || mBuffering; - if (DEBUG) Log.v(TAG, (mPaused ? "paused" : "playing") + " at " + mLastTimeUs); - } catch (IllegalStateException e) { - if (mPausing) { - // if we were pausing, get last estimated timestamp - mPausing = false; - getEstimatedTime(nanoTime, monotonic); - mPaused = true; - if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime); - return mLastReportedTime; + try { + mLastTimeUs = mPlayer.getCurrentPosition() * 1000L; + mPaused = !mPlayer.isPlaying() || mBuffering; + if (DEBUG) Log.v(TAG, (mPaused ? "paused" : "playing") + " at " + mLastTimeUs); + } catch (IllegalStateException e) { + if (mPausing) { + // if we were pausing, get last estimated timestamp + mPausing = false; + if (!monotonic || mLastReportedTime < mLastTimeUs) { + mLastReportedTime = mLastTimeUs; } - // TODO get time when prepared - throw e; + mPaused = true; + if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime); + return mLastReportedTime; } - mLastNanoTime = nanoTime; - if (monotonic && mLastTimeUs < mLastReportedTime) { - /* have to adjust time */ - mTimeAdjustment = mLastReportedTime - mLastTimeUs; - if (mTimeAdjustment > 1000000) { - // schedule seeked event if time jumped significantly - // TODO: do this properly by introducing an exception - mStopped = false; - mSeeking = true; - scheduleNotification(NOTIFY_SEEK, 0 /* delay */); - } - } else { - mTimeAdjustment = 0; + // TODO get time when prepared + throw e; + } + if (monotonic && mLastTimeUs < mLastReportedTime) { + /* have to adjust time */ + if (mLastReportedTime - mLastTimeUs > 1000000) { + // schedule seeked event if time jumped significantly + // TODO: do this properly by introducing an exception + mStopped = false; + mSeeking = true; + scheduleNotification(NOTIFY_SEEK, 0 /* delay */); } + } else { + mLastReportedTime = mLastTimeUs; } - return getEstimatedTime(nanoTime, monotonic); + return mLastReportedTime; } } @@ -5526,9 +5651,6 @@ public class MediaPlayer extends PlayerBase if (msg.what == NOTIFY) { switch (msg.arg1) { case NOTIFY_TIME: - notifyTimedEvent(false /* refreshTime */); - break; - case REFRESH_AND_NOTIFY_TIME: notifyTimedEvent(true /* refreshTime */); break; case NOTIFY_STOP: diff --git a/media/java/android/media/MediaPlayer2.java b/media/java/android/media/MediaPlayer2.java new file mode 100644 index 000000000000..e331b2cbf645 --- /dev/null +++ b/media/java/android/media/MediaPlayer2.java @@ -0,0 +1,2438 @@ +/* + * Copyright 2017 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.annotation.CallbackExecutor; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.net.Uri; +import android.os.Handler; +import android.os.Parcel; +import android.os.PersistableBundle; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.media.MediaDrm; +import android.media.MediaFormat; +import android.media.MediaPlayer2Impl; +import android.media.MediaTimeProvider; +import android.media.PlaybackParams; +import android.media.SubtitleController; +import android.media.SubtitleController.Anchor; +import android.media.SubtitleData; +import android.media.SubtitleTrack.RenderingWidget; +import android.media.SyncParams; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.lang.AutoCloseable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.InetSocketAddress; +import java.util.concurrent.Executor; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + + +/** + * MediaPlayer2 class can be used to control playback + * of audio/video files and streams. An example on how to use the methods in + * this class can be found in {@link android.widget.VideoView}. + * + * <p>Topics covered here are: + * <ol> + * <li><a href="#StateDiagram">State Diagram</a> + * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a> + * <li><a href="#Permissions">Permissions</a> + * <li><a href="#Callbacks">Register informational and error callbacks</a> + * </ol> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about how to use MediaPlayer2, read the + * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p> + * </div> + * + * <a name="StateDiagram"></a> + * <h3>State Diagram</h3> + * + * <p>Playback control of audio/video files and streams is managed as a state + * machine. The following diagram shows the life cycle and the states of a + * MediaPlayer2 object driven by the supported playback control operations. + * The ovals represent the states a MediaPlayer2 object may reside + * in. The arcs represent the playback control operations that drive the object + * state transition. There are two types of arcs. The arcs with a single arrow + * head represent synchronous method calls, while those with + * a double arrow head represent asynchronous method calls.</p> + * + * <p><img src="../../../images/mediaplayer_state_diagram.gif" + * alt="MediaPlayer State diagram" + * border="0" /></p> + * + * <p>From this state diagram, one can see that a MediaPlayer2 object has the + * following states:</p> + * <ul> + * <li>When a MediaPlayer2 object is just created using <code>new</code> or + * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after + * {@link #close()} is called, it is in the <em>End</em> state. Between these + * two states is the life cycle of the MediaPlayer2 object. + * <ul> + * <li> It is a programming error to invoke methods such + * as {@link #getCurrentPosition()}, + * {@link #getDuration()}, {@link #getVideoHeight()}, + * {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)}, + * {@link #setVolume(float, float)}, {@link #pause()}, {@link #play()}, + * {@link #seekTo(long, int)} or + * {@link #prepareAsync()} in the <em>Idle</em> state. + * <li>It is also recommended that once + * a MediaPlayer2 object is no longer being used, call {@link #close()} immediately + * so that resources used by the internal player engine associated with the + * MediaPlayer2 object can be released immediately. Resource may include + * singleton resources such as hardware acceleration components and + * failure to call {@link #close()} may cause subsequent instances of + * MediaPlayer2 objects to fallback to software implementations or fail + * altogether. Once the MediaPlayer2 + * object is in the <em>End</em> state, it can no longer be used and + * there is no way to bring it back to any other state. </li> + * <li>Furthermore, + * the MediaPlayer2 objects created using <code>new</code> is in the + * <em>Idle</em> state. + * </li> + * </ul> + * </li> + * <li>In general, some playback control operation may fail due to various + * reasons, such as unsupported audio/video format, poorly interleaved + * audio/video, resolution too high, streaming timeout, and the like. + * Thus, error reporting and recovery is an important concern under + * these circumstances. Sometimes, due to programming errors, invoking a playback + * control operation in an invalid state may also occur. Under all these + * error conditions, the internal player engine invokes a user supplied + * EventCallback.onError() method if an EventCallback has been + * registered beforehand via + * {@link #registerEventCallback(Executor, EventCallback)}. + * <ul> + * <li>It is important to note that once an error occurs, the + * MediaPlayer2 object enters the <em>Error</em> state (except as noted + * above), even if an error listener has not been registered by the application.</li> + * <li>In order to reuse a MediaPlayer2 object that is in the <em> + * Error</em> state and recover from the error, + * {@link #reset()} can be called to restore the object to its <em>Idle</em> + * state.</li> + * <li>It is good programming practice to have your application + * register a OnErrorListener to look out for error notifications from + * the internal player engine.</li> + * <li>IllegalStateException is + * thrown to prevent programming errors such as calling + * {@link #prepareAsync()}, {@link #setDataSource(DataSourceDesc)}, or + * {@code setPlaylist} methods in an invalid state. </li> + * </ul> + * </li> + * <li>Calling + * {@link #setDataSource(DataSourceDesc)}, or + * {@code setPlaylist} transfers a + * MediaPlayer2 object in the <em>Idle</em> state to the + * <em>Initialized</em> state. + * <ul> + * <li>An IllegalStateException is thrown if + * setDataSource() or setPlaylist() is called in any other state.</li> + * <li>It is good programming + * practice to always look out for <code>IllegalArgumentException</code> + * and <code>IOException</code> that may be thrown from + * <code>setDataSource</code> and <code>setPlaylist</code> methods.</li> + * </ul> + * </li> + * <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state + * before playback can be started. + * <ul> + * <li>There are an asynchronous way that the <em>Prepared</em> state can be reached: + * a call to {@link #prepareAsync()} (asynchronous) which + * first transfers the object to the <em>Preparing</em> state after the + * call returns (which occurs almost right way) while the internal + * player engine continues working on the rest of preparation work + * until the preparation work completes. When the preparation completes, + * the internal player engine then calls a user supplied callback method, + * onInfo() of the EventCallback interface with {@link #MEDIA_INFO_PREPARED}, if an + * EventCallback is registered beforehand via + * {@link #registerEventCallback(Executor, EventCallback)}.</li> + * <li>It is important to note that + * the <em>Preparing</em> state is a transient state, and the behavior + * of calling any method with side effect while a MediaPlayer2 object is + * in the <em>Preparing</em> state is undefined.</li> + * <li>An IllegalStateException is + * thrown if {@link #prepareAsync()} is called in + * any other state.</li> + * <li>While in the <em>Prepared</em> state, properties + * such as audio/sound volume, screenOnWhilePlaying, looping can be + * adjusted by invoking the corresponding set methods.</li> + * </ul> + * </li> + * <li>To start the playback, {@link #play()} must be called. After + * {@link #play()} returns successfully, the MediaPlayer2 object is in the + * <em>Started</em> state. {@link #isPlaying()} can be called to test + * whether the MediaPlayer2 object is in the <em>Started</em> state. + * <ul> + * <li>While in the <em>Started</em> state, the internal player engine calls + * a user supplied EventCallback.onBufferingUpdate() callback + * method if an EventCallback has been registered beforehand + * via {@link #registerEventCallback(Executor, EventCallback)}. + * This callback allows applications to keep track of the buffering status + * while streaming audio/video.</li> + * <li>Calling {@link #play()} has not effect + * on a MediaPlayer2 object that is already in the <em>Started</em> state.</li> + * </ul> + * </li> + * <li>Playback can be paused and stopped, and the current playback position + * can be adjusted. Playback can be paused via {@link #pause()}. When the call to + * {@link #pause()} returns, the MediaPlayer2 object enters the + * <em>Paused</em> state. Note that the transition from the <em>Started</em> + * state to the <em>Paused</em> state and vice versa happens + * asynchronously in the player engine. It may take some time before + * the state is updated in calls to {@link #isPlaying()}, and it can be + * a number of seconds in the case of streamed content. + * <ul> + * <li>Calling {@link #play()} to resume playback for a paused + * MediaPlayer2 object, and the resumed playback + * position is the same as where it was paused. When the call to + * {@link #play()} returns, the paused MediaPlayer2 object goes back to + * the <em>Started</em> state.</li> + * <li>Calling {@link #pause()} has no effect on + * a MediaPlayer2 object that is already in the <em>Paused</em> state.</li> + * </ul> + * </li> + * <li>The playback position can be adjusted with a call to + * {@link #seekTo(long, int)}. + * <ul> + * <li>Although the asynchronuous {@link #seekTo(long, int)} + * call returns right away, the actual seek operation may take a while to + * finish, especially for audio/video being streamed. When the actual + * seek operation completes, the internal player engine calls a user + * supplied EventCallback.onInfo() with {@link #MEDIA_INFO_COMPLETE_CALL_SEEK} + * if an EventCallback has been registered beforehand via + * {@link #registerEventCallback(Executor, EventCallback)}.</li> + * <li>Please + * note that {@link #seekTo(long, int)} can also be called in the other states, + * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted + * </em> state. When {@link #seekTo(long, int)} is called in those states, + * one video frame will be displayed if the stream has video and the requested + * position is valid. + * </li> + * <li>Furthermore, the actual current playback position + * can be retrieved with a call to {@link #getCurrentPosition()}, which + * is helpful for applications such as a Music player that need to keep + * track of the playback progress.</li> + * </ul> + * </li> + * <li>When the playback reaches the end of stream, the playback completes. + * <ul> + * <li>If the looping mode was being set to one of the values of + * {@link #LOOPING_MODE_FULL}, {@link #LOOPING_MODE_SINGLE} or + * {@link #LOOPING_MODE_SHUFFLE} with + * {@link #setLoopingMode(int)}, the MediaPlayer2 object shall remain in + * the <em>Started</em> state.</li> + * <li>If the looping mode was set to <var>false + * </var>, the player engine calls a user supplied callback method, + * EventCallback.onCompletion(), if an EventCallback is registered + * beforehand via {@link #registerEventCallback(Executor, EventCallback)}. + * The invoke of the callback signals that the object is now in the <em> + * PlaybackCompleted</em> state.</li> + * <li>While in the <em>PlaybackCompleted</em> + * state, calling {@link #play()} can restart the playback from the + * beginning of the audio/video source.</li> + * </ul> + * + * + * <a name="Valid_and_Invalid_States"></a> + * <h3>Valid and invalid states</h3> + * + * <table border="0" cellspacing="0" cellpadding="0"> + * <tr><td>Method Name </p></td> + * <td>Valid Sates </p></td> + * <td>Invalid States </p></td> + * <td>Comments </p></td></tr> + * <tr><td>attachAuxEffect </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td> + * <td>{Idle, Error} </p></td> + * <td>This method must be called after setDataSource or setPlaylist. + * Calling it does not change the object state. </p></td></tr> + * <tr><td>getAudioSessionId </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>getCurrentPosition </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted} </p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getDuration </p></td> + * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td> + * <td>{Idle, Initialized, Error} </p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getVideoHeight </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getVideoWidth </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>isPlaying </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>pause </p></td> + * <td>{Started, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Paused</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>prepareAsync </p></td> + * <td>{Initialized, Stopped} </p></td> + * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Preparing</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>release </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>After {@link #close()}, the object is no longer available. </p></td></tr> + * <tr><td>reset </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted, Error}</p></td> + * <td>{}</p></td> + * <td>After {@link #reset()}, the object is like being just created.</p></td></tr> + * <tr><td>seekTo </p></td> + * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td> + * <td>{Idle, Initialized, Stopped, Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>setAudioAttributes </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method does not change the state. In order for the + * target audio attributes type to become effective, this method must be called before + * prepareAsync().</p></td></tr> + * <tr><td>setAudioSessionId </p></td> + * <td>{Idle} </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, + * Error} </p></td> + * <td>This method must be called in idle state as the audio session ID must be known before + * calling setDataSource or setPlaylist. Calling it does not change the object + * state. </p></td></tr> + * <tr><td>setAudioStreamType (deprecated)</p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method does not change the state. In order for the + * target audio stream type to become effective, this method must be called before + * prepareAsync().</p></td></tr> + * <tr><td>setAuxEffectSendLevel </p></td> + * <td>any</p></td> + * <td>{} </p></td> + * <td>Calling this method does not change the object state. </p></td></tr> + * <tr><td>setDataSource </p></td> + * <td>{Idle} </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, + * Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Initialized</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>setPlaylist </p></td> + * <td>{Idle} </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, + * Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Initialized</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>setDisplay </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setSurface </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setLoopingMode </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>isLooping </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>registerDrmEventCallback </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>registerEventCallback </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setPlaybackParams</p></td> + * <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td> + * <td>{Idle, Stopped} </p></td> + * <td>This method will change state in some cases, depending on when it's called. + * </p></td></tr> + * <tr><td>setVolume </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method does not change the state. + * <tr><td>play </p></td> + * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Stopped, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Started</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>stop </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Stopped</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>getTrackInfo </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * <tr><td>selectTrack </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * <tr><td>deselectTrack </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * + * </table> + * + * <a name="Permissions"></a> + * <h3>Permissions</h3> + * <p>One may need to declare a corresponding WAKE_LOCK permission {@link + * android.R.styleable#AndroidManifestUsesPermission <uses-permission>} + * element. + * + * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission + * when used with network-based content. + * + * <a name="Callbacks"></a> + * <h3>Callbacks</h3> + * <p>Applications may want to register for informational and error + * events in order to be informed of some internal state update and + * possible runtime errors during playback or streaming. Registration for + * these events is done by properly setting the appropriate listeners (via calls + * to + * {@link #registerEventCallback(Executor, EventCallback)}, + * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}). + * In order to receive the respective callback + * associated with these listeners, applications are required to create + * MediaPlayer2 objects on a thread with its own Looper running (main UI + * thread by default has a Looper running). + * + */ +public abstract class MediaPlayer2 implements SubtitleController.Listener + , AudioRouting + , AutoCloseable +{ + /** + Constant to retrieve only the new metadata since the last + call. + // FIXME: unhide. + // FIXME: add link to getMetadata(boolean, boolean) + {@hide} + */ + public static final boolean METADATA_UPDATE_ONLY = true; + + /** + Constant to retrieve all the metadata. + // FIXME: unhide. + // FIXME: add link to getMetadata(boolean, boolean) + {@hide} + */ + public static final boolean METADATA_ALL = false; + + /** + Constant to enable the metadata filter during retrieval. + // FIXME: unhide. + // FIXME: add link to getMetadata(boolean, boolean) + {@hide} + */ + public static final boolean APPLY_METADATA_FILTER = true; + + /** + Constant to disable the metadata filter during retrieval. + // FIXME: unhide. + // FIXME: add link to getMetadata(boolean, boolean) + {@hide} + */ + public static final boolean BYPASS_METADATA_FILTER = false; + + /** + * Create a MediaPlayer2 object. + * + * @return A MediaPlayer2 object created + */ + public static final MediaPlayer2 create() { + // TODO: load MediaUpdate APK + return new MediaPlayer2Impl(); + } + + /** + * @hide + */ + // add hidden empty constructor so it doesn't show in SDK + public MediaPlayer2() { } + + /** + * Create a request parcel which can be routed to the native media + * player using {@link #invoke(Parcel, Parcel)}. The Parcel + * returned has the proper InterfaceToken set. The caller should + * not overwrite that token, i.e it can only append data to the + * Parcel. + * + * @return A parcel suitable to hold a request for the native + * player. + * {@hide} + */ + public Parcel newRequest() { + return null; + } + + /** + * Invoke a generic method on the native player using opaque + * parcels for the request and reply. Both payloads' format is a + * convention between the java caller and the native player. + * Must be called after setDataSource or setPlaylist to make sure a native player + * exists. On failure, a RuntimeException is thrown. + * + * @param request Parcel with the data for the extension. The + * caller must use {@link #newRequest()} to get one. + * + * @param reply Output parcel with the data returned by the + * native player. + * {@hide} + */ + public void invoke(Parcel request, Parcel reply) { } + + /** + * Sets the {@link SurfaceHolder} to use for displaying the video + * portion of the media. + * + * Either a surface holder or surface must be set if a display or video sink + * is needed. Not calling this method or {@link #setSurface(Surface)} + * when playing back a video will result in only the audio track being played. + * A null surface holder or surface will result in only the audio track being + * played. + * + * @param sh the SurfaceHolder to use for video display + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released. + * @hide + */ + public abstract void setDisplay(SurfaceHolder sh); + + /** + * Sets the {@link Surface} to be used as the sink for the video portion of + * the media. Setting a + * Surface will un-set any Surface or SurfaceHolder that was previously set. + * A null surface will result in only the audio track being played. + * + * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps + * returned from {@link SurfaceTexture#getTimestamp()} will have an + * unspecified zero point. These timestamps cannot be directly compared + * between different media sources, different instances of the same media + * source, or multiple runs of the same program. The timestamp is normally + * monotonically increasing and is unaffected by time-of-day adjustments, + * but it is reset when the position is set. + * + * @param surface The {@link Surface} to be used for the video portion of + * the media. + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released. + */ + public abstract void setSurface(Surface surface); + + /* Do not change these video scaling mode values below without updating + * their counterparts in system/window.h! Please do not forget to update + * {@link #isVideoScalingModeSupported} when new video scaling modes + * are added. + */ + /** + * Specifies a video scaling mode. The content is stretched to the + * surface rendering area. When the surface has the same aspect ratio + * as the content, the aspect ratio of the content is maintained; + * otherwise, the aspect ratio of the content is not maintained when video + * is being rendered. + * There is no content cropping with this video scaling mode. + */ + public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1; + + /** + * Specifies a video scaling mode. The content is scaled, maintaining + * its aspect ratio. The whole surface area is always used. When the + * aspect ratio of the content is the same as the surface, no content + * is cropped; otherwise, content is cropped to fit the surface. + * @hide + */ + public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = 2; + + /** + * Sets video scaling mode. To make the target video scaling mode + * effective during playback, this method must be called after + * data source is set. If not called, the default video + * scaling mode is {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}. + * + * <p> The supported video scaling modes are: + * <ul> + * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT} + * </ul> + * + * @param mode target video scaling mode. Must be one of the supported + * video scaling modes; otherwise, IllegalArgumentException will be thrown. + * + * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT + * @hide + */ + public void setVideoScalingMode(int mode) { } + + /** + * Discards all pending commands. + */ + public abstract void clearPendingCommands(); + + /** + * Sets the data source as described by a DataSourceDesc. + * + * @param dsd the descriptor of data source you want to play + * @throws IllegalStateException if it is called in an invalid state + * @throws NullPointerException if dsd is null + */ + public abstract void setDataSource(@NonNull DataSourceDesc dsd) throws IOException; + + /** + * Gets the current data source as described by a DataSourceDesc. + * + * @return the current DataSourceDesc + */ + public abstract DataSourceDesc getCurrentDataSource(); + + /** + * Sets the play list. + * + * If startIndex falls outside play list range, it will be clamped to the nearest index + * in the play list. + * + * @param pl the play list of data source you want to play + * @param startIndex the index of the DataSourceDesc in the play list you want to play first + * @throws IllegalStateException if it is called in an invalid state + * @throws IllegalArgumentException if pl is null or empty, or pl contains null DataSourceDesc + */ + public abstract void setPlaylist(@NonNull List<DataSourceDesc> pl, int startIndex) + throws IOException; + + /** + * Gets a copy of the play list. + * + * @return a copy of the play list used by {@link MediaPlayer2} + */ + public abstract List<DataSourceDesc> getPlaylist(); + + /** + * Sets the index of current DataSourceDesc in the play list to be played. + * + * @param index the index of DataSourceDesc in the play list you want to play + * @throws IllegalArgumentException if the play list is null + * @throws NullPointerException if index is outside play list range + */ + public abstract void setCurrentPlaylistItem(int index); + + /** + * Sets the index of next-to-be-played DataSourceDesc in the play list. + * + * @param index the index of next-to-be-played DataSourceDesc in the play list + * @throws IllegalArgumentException if the play list is null + * @throws NullPointerException if index is outside play list range + */ + public abstract void setNextPlaylistItem(int index); + + /** + * Gets the current index of play list. + * + * @return the index of the current DataSourceDesc in the play list + */ + public abstract int getCurrentPlaylistItemIndex(); + + /** + * Specifies a playback looping mode. The source will not be played in looping mode. + */ + public static final int LOOPING_MODE_NONE = 0; + /** + * Specifies a playback looping mode. The full list of source will be played in looping mode, + * and in the order specified in the play list. + */ + public static final int LOOPING_MODE_FULL = 1; + /** + * Specifies a playback looping mode. The current DataSourceDesc will be played in looping mode. + */ + public static final int LOOPING_MODE_SINGLE = 2; + /** + * Specifies a playback looping mode. The full list of source will be played in looping mode, + * and in a random order. + */ + public static final int LOOPING_MODE_SHUFFLE = 3; + + /** @hide */ + @IntDef( + value = { + LOOPING_MODE_NONE, + LOOPING_MODE_FULL, + LOOPING_MODE_SINGLE, + LOOPING_MODE_SHUFFLE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface LoopingMode {} + + /** + * Sets the looping mode of the play list. + * The mode shall be one of {@link #LOOPING_MODE_NONE}, {@link #LOOPING_MODE_FULL}, + * {@link #LOOPING_MODE_SINGLE}, {@link #LOOPING_MODE_SHUFFLE}. + * + * @param mode the mode in which the play list will be played + * @throws IllegalArgumentException if mode is not supported + */ + public abstract void setLoopingMode(@LoopingMode int mode); + + /** + * Gets the looping mode of play list. + * + * @return the looping mode of the play list + */ + public abstract int getLoopingMode(); + + /** + * Moves the DataSourceDesc at indexFrom in the play list to indexTo. + * + * @throws IllegalArgumentException if the play list is null + * @throws IndexOutOfBoundsException if indexFrom or indexTo is outside play list range + */ + public abstract void movePlaylistItem(int indexFrom, int indexTo); + + /** + * Removes the DataSourceDesc at index in the play list. + * + * If index is same as the current index of the play list, current DataSourceDesc + * will be stopped and playback moves to next source in the list. + * + * @return the removed DataSourceDesc at index in the play list + * @throws IllegalArgumentException if the play list is null + * @throws IndexOutOfBoundsException if index is outside play list range + */ + public abstract DataSourceDesc removePlaylistItem(int index); + + /** + * Inserts the DataSourceDesc to the play list at position index. + * + * This will not change the DataSourceDesc currently being played. + * If index is less than or equal to the current index of the play list, + * the current index of the play list will be incremented correspondingly. + * + * @param index the index you want to add dsd to the play list + * @param dsd the descriptor of data source you want to add to the play list + * @throws IndexOutOfBoundsException if index is outside play list range + * @throws NullPointerException if dsd is null + */ + public abstract void addPlaylistItem(int index, DataSourceDesc dsd); + + /** + * replaces the DataSourceDesc at index in the play list with given dsd. + * + * When index is same as the current index of the play list, the current source + * will be stopped and the new source will be played, except that if new + * and old source only differ on end position and current media position is + * smaller then the new end position. + * + * This will not change the DataSourceDesc currently being played. + * If index is less than or equal to the current index of the play list, + * the current index of the play list will be incremented correspondingly. + * + * @param index the index you want to add dsd to the play list + * @param dsd the descriptor of data source you want to add to the play list + * @throws IndexOutOfBoundsException if index is outside play list range + * @throws NullPointerException if dsd is null + */ + public abstract DataSourceDesc editPlaylistItem(int index, DataSourceDesc dsd); + + /** + * Prepares the player for playback, synchronously. + * + * After setting the datasource and the display surface, you need to either + * call prepare() or prepareAsync(). For files, it is OK to call prepare(), + * which blocks until MediaPlayer2 is ready for playback. + * + * @throws IOException if source can not be accessed + * @throws IllegalStateException if it is called in an invalid state + * @hide + */ + public void prepare() throws IOException { } + + /** + * Prepares the player for playback, asynchronously. + * + * After setting the datasource and the display surface, you need to + * call prepareAsync(). + * + * @throws IllegalStateException if it is called in an invalid state + */ + public abstract void prepareAsync(); + + /** + * Starts or resumes playback. If playback had previously been paused, + * playback will continue from where it was paused. If playback had + * been stopped, or never started before, playback will start at the + * beginning. + * + * @throws IllegalStateException if it is called in an invalid state + */ + public abstract void play(); + + /** + * Stops playback after playback has been started or paused. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + * @hide + */ + public void stop() { } + + /** + * Pauses playback. Call play() to resume. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + */ + public abstract void pause(); + + //-------------------------------------------------------------------------- + // Explicit Routing + //-------------------- + + /** + * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route + * the output from this MediaPlayer2. + * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source. + * If deviceInfo is null, default routing is restored. + * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and + * does not correspond to a valid audio device. + */ + @Override + public abstract boolean setPreferredDevice(AudioDeviceInfo deviceInfo); + + /** + * Returns the selected output specified by {@link #setPreferredDevice}. Note that this + * is not guaranteed to correspond to the actual device being used for playback. + */ + @Override + public abstract AudioDeviceInfo getPreferredDevice(); + + /** + * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer2 + * Note: The query is only valid if the MediaPlayer2 is currently playing. + * If the player is not playing, the returned device can be null or correspond to previously + * selected device when the player was last active. + */ + @Override + public abstract AudioDeviceInfo getRoutedDevice(); + + /** + * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing + * changes on this MediaPlayer2. + * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive + * notifications of rerouting events. + * @param handler Specifies the {@link Handler} object for the thread on which to execute + * the callback. If <code>null</code>, the handler on the main looper will be used. + */ + @Override + public abstract void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener, + Handler handler); + + /** + * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added + * to receive rerouting notifications. + * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface + * to remove. + */ + @Override + public abstract void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener); + + /** + * Set the low-level power management behavior for this MediaPlayer2. + * + * <p>This function has the MediaPlayer2 access the low-level power manager + * service to control the device's power usage while playing is occurring. + * The parameter is a combination of {@link android.os.PowerManager} wake flags. + * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK} + * permission. + * By default, no attempt is made to keep the device awake during playback. + * + * @param context the Context to use + * @param mode the power/wake mode to set + * @see android.os.PowerManager + * @hide + */ + public abstract void setWakeMode(Context context, int mode); + + /** + * Control whether we should use the attached SurfaceHolder to keep the + * screen on while video playback is occurring. This is the preferred + * method over {@link #setWakeMode} where possible, since it doesn't + * require that the application have permission for low-level wake lock + * access. + * + * @param screenOn Supply true to keep the screen on, false to allow it + * to turn off. + * @hide + */ + public abstract void setScreenOnWhilePlaying(boolean screenOn); + + /** + * Returns the width of the video. + * + * @return the width of the video, or 0 if there is no video, + * no display surface was set, or the width has not been determined + * yet. The {@code EventCallback} can be registered via + * {@link #registerEventCallback(Executor, EventCallback)} to provide a + * notification {@code EventCallback.onVideoSizeChanged} when the width is available. + */ + public abstract int getVideoWidth(); + + /** + * Returns the height of the video. + * + * @return the height of the video, or 0 if there is no video, + * no display surface was set, or the height has not been determined + * yet. The {@code EventCallback} can be registered via + * {@link #registerEventCallback(Executor, EventCallback)} to provide a + * notification {@code EventCallback.onVideoSizeChanged} when the height is available. + */ + public abstract int getVideoHeight(); + + /** + * Return Metrics data about the current player. + * + * @return a {@link PersistableBundle} containing the set of attributes and values + * available for the media being handled by this instance of MediaPlayer2 + * The attributes are descibed in {@link MetricsConstants}. + * + * Additional vendor-specific fields may also be present in + * the return value. + */ + public abstract PersistableBundle getMetrics(); + + /** + * Checks whether the MediaPlayer2 is playing. + * + * @return true if currently playing, false otherwise + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released. + */ + public abstract boolean isPlaying(); + + /** + * Gets the current buffering management params used by the source component. + * Calling it only after {@code setDataSource} has been called. + * Each type of data source might have different set of default params. + * + * @return the current buffering management params used by the source component. + * @throws IllegalStateException if the internal player engine has not been + * initialized, or {@code setDataSource} has not been called. + * @hide + */ + @NonNull + public BufferingParams getBufferingParams() { + return new BufferingParams.Builder().build(); + } + + /** + * Sets buffering management params. + * The object sets its internal BufferingParams to the input, except that the input is + * invalid or not supported. + * Call it only after {@code setDataSource} has been called. + * The input is a hint to MediaPlayer2. + * + * @param params the buffering management params. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released, or {@code setDataSource} has not been called. + * @throws IllegalArgumentException if params is invalid or not supported. + * @hide + */ + public void setBufferingParams(@NonNull BufferingParams params) { } + + /** + * Change playback speed of audio by resampling the audio. + * <p> + * Specifies resampling as audio mode for variable rate playback, i.e., + * resample the waveform based on the requested playback rate to get + * a new waveform, and play back the new waveform at the original sampling + * frequency. + * When rate is larger than 1.0, pitch becomes higher. + * When rate is smaller than 1.0, pitch becomes lower. + * + * @hide + */ + public static final int PLAYBACK_RATE_AUDIO_MODE_RESAMPLE = 2; + + /** + * Change playback speed of audio without changing its pitch. + * <p> + * Specifies time stretching as audio mode for variable rate playback. + * Time stretching changes the duration of the audio samples without + * affecting its pitch. + * <p> + * This mode is only supported for a limited range of playback speed factors, + * e.g. between 1/2x and 2x. + * + * @hide + */ + public static final int PLAYBACK_RATE_AUDIO_MODE_STRETCH = 1; + + /** + * Change playback speed of audio without changing its pitch, and + * possibly mute audio if time stretching is not supported for the playback + * speed. + * <p> + * Try to keep audio pitch when changing the playback rate, but allow the + * system to determine how to change audio playback if the rate is out + * of range. + * + * @hide + */ + public static final int PLAYBACK_RATE_AUDIO_MODE_DEFAULT = 0; + + /** @hide */ + @IntDef( + value = { + PLAYBACK_RATE_AUDIO_MODE_DEFAULT, + PLAYBACK_RATE_AUDIO_MODE_STRETCH, + PLAYBACK_RATE_AUDIO_MODE_RESAMPLE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PlaybackRateAudioMode {} + + /** + * Sets playback rate and audio mode. + * + * @param rate the ratio between desired playback rate and normal one. + * @param audioMode audio playback mode. Must be one of the supported + * audio modes. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + * @throws IllegalArgumentException if audioMode is not supported. + * + * @hide + */ + @NonNull + public PlaybackParams easyPlaybackParams(float rate, @PlaybackRateAudioMode int audioMode) { + return new PlaybackParams(); + } + + /** + * Sets playback rate using {@link PlaybackParams}. The object sets its internal + * PlaybackParams to the input, except that the object remembers previous speed + * when input speed is zero. This allows the object to resume at previous speed + * when play() is called. Calling it before the object is prepared does not change + * the object state. After the object is prepared, calling it with zero speed is + * equivalent to calling pause(). After the object is prepared, calling it with + * non-zero speed is equivalent to calling play(). + * + * @param params the playback params. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released. + * @throws IllegalArgumentException if params is not supported. + */ + public abstract void setPlaybackParams(@NonNull PlaybackParams params); + + /** + * Gets the playback params, containing the current playback rate. + * + * @return the playback params. + * @throws IllegalStateException if the internal player engine has not been + * initialized. + */ + @NonNull + public abstract PlaybackParams getPlaybackParams(); + + /** + * Sets A/V sync mode. + * + * @param params the A/V sync params to apply + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + * @throws IllegalArgumentException if params are not supported. + */ + public abstract void setSyncParams(@NonNull SyncParams params); + + /** + * Gets the A/V sync mode. + * + * @return the A/V sync params + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + */ + @NonNull + public abstract SyncParams getSyncParams(); + + /** + * Seek modes used in method seekTo(long, int) to move media position + * to a specified location. + * + * Do not change these mode values without updating their counterparts + * in include/media/IMediaSource.h! + */ + /** + * This mode is used with {@link #seekTo(long, int)} to move media position to + * a sync (or key) frame associated with a data source that is located + * right before or at the given time. + * + * @see #seekTo(long, int) + */ + public static final int SEEK_PREVIOUS_SYNC = 0x00; + /** + * This mode is used with {@link #seekTo(long, int)} to move media position to + * a sync (or key) frame associated with a data source that is located + * right after or at the given time. + * + * @see #seekTo(long, int) + */ + public static final int SEEK_NEXT_SYNC = 0x01; + /** + * This mode is used with {@link #seekTo(long, int)} to move media position to + * a sync (or key) frame associated with a data source that is located + * closest to (in time) or at the given time. + * + * @see #seekTo(long, int) + */ + public static final int SEEK_CLOSEST_SYNC = 0x02; + /** + * This mode is used with {@link #seekTo(long, int)} to move media position to + * a frame (not necessarily a key frame) associated with a data source that + * is located closest to or at the given time. + * + * @see #seekTo(long, int) + */ + public static final int SEEK_CLOSEST = 0x03; + + /** @hide */ + @IntDef( + value = { + SEEK_PREVIOUS_SYNC, + SEEK_NEXT_SYNC, + SEEK_CLOSEST_SYNC, + SEEK_CLOSEST, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SeekMode {} + + /** + * Moves the media to specified time position by considering the given mode. + * <p> + * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user. + * There is at most one active seekTo processed at any time. If there is a to-be-completed + * seekTo, new seekTo requests will be queued in such a way that only the last request + * is kept. When current seekTo is completed, the queued request will be processed if + * that request is different from just-finished seekTo operation, i.e., the requested + * position or mode is different. + * + * @param msec the offset in milliseconds from the start to seek to. + * When seeking to the given time position, there is no guarantee that the data source + * has a frame located at the position. When this happens, a frame nearby will be rendered. + * If msec is negative, time position zero will be used. + * If msec is larger than duration, duration will be used. + * @param mode the mode indicating where exactly to seek to. + * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame + * that has a timestamp earlier than or the same as msec. Use + * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame + * that has a timestamp later than or the same as msec. Use + * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame + * that has a timestamp closest to or the same as msec. Use + * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may + * or may not be a sync frame but is closest to or the same as msec. + * {@link #SEEK_CLOSEST} often has larger performance overhead compared + * to the other options if there is no sync frame located at msec. + * @throws IllegalStateException if the internal player engine has not been + * initialized + * @throws IllegalArgumentException if the mode is invalid. + */ + public abstract void seekTo(long msec, @SeekMode int mode); + + /** + * Get current playback position as a {@link MediaTimestamp}. + * <p> + * The MediaTimestamp represents how the media time correlates to the system time in + * a linear fashion using an anchor and a clock rate. During regular playback, the media + * time moves fairly constantly (though the anchor frame may be rebased to a current + * system time, the linear correlation stays steady). Therefore, this method does not + * need to be called often. + * <p> + * To help users get current playback position, this method always anchors the timestamp + * to the current {@link System#nanoTime system time}, so + * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position. + * + * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp + * is available, e.g. because the media player has not been initialized. + * + * @see MediaTimestamp + */ + @Nullable + public abstract MediaTimestamp getTimestamp(); + + /** + * Gets the current playback position. + * + * @return the current position in milliseconds + */ + public abstract int getCurrentPosition(); + + /** + * Gets the duration of the file. + * + * @return the duration in milliseconds, if no duration is available + * (for example, if streaming live content), -1 is returned. + */ + public abstract int getDuration(); + + /** + * Gets the media metadata. + * + * @param update_only controls whether the full set of available + * metadata is returned or just the set that changed since the + * last call. See {@see #METADATA_UPDATE_ONLY} and {@see + * #METADATA_ALL}. + * + * @param apply_filter if true only metadata that matches the + * filter is returned. See {@see #APPLY_METADATA_FILTER} and {@see + * #BYPASS_METADATA_FILTER}. + * + * @return The metadata, possibly empty. null if an error occured. + // FIXME: unhide. + * {@hide} + */ + public Metadata getMetadata(final boolean update_only, + final boolean apply_filter) { + return null; + } + + /** + * Set a filter for the metadata update notification and update + * retrieval. The caller provides 2 set of metadata keys, allowed + * and blocked. The blocked set always takes precedence over the + * allowed one. + * Metadata.MATCH_ALL and Metadata.MATCH_NONE are 2 sets available as + * shorthands to allow/block all or no metadata. + * + * By default, there is no filter set. + * + * @param allow Is the set of metadata the client is interested + * in receiving new notifications for. + * @param block Is the set of metadata the client is not interested + * in receiving new notifications for. + * @return The call status code. + * + // FIXME: unhide. + * {@hide} + */ + public int setMetadataFilter(Set<Integer> allow, Set<Integer> block) { + return 0; + } + + /** + * Set the MediaPlayer2 to start when this MediaPlayer2 finishes playback + * (i.e. reaches the end of the stream). + * The media framework will attempt to transition from this player to + * the next as seamlessly as possible. The next player can be set at + * any time before completion, but shall be after setDataSource has been + * called successfully. The next player must be prepared by the + * app, and the application should not call play() on it. + * The next MediaPlayer2 must be different from 'this'. An exception + * will be thrown if next == this. + * The application may call setNextMediaPlayer(null) to indicate no + * next player should be started at the end of playback. + * If the current player is looping, it will keep looping and the next + * player will not be started. + * + * @param next the player to start after this one completes playback. + * + * @hide + */ + public void setNextMediaPlayer(MediaPlayer2 next) { } + + /** + * Resets the MediaPlayer2 to its uninitialized state. After calling + * this method, you will have to initialize it again by setting the + * data source and calling prepareAsync(). + */ + public abstract void reset(); + + /** + * Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be + * notified when the presentation time reaches (becomes greater than or equal to) + * the value specified. + * + * @param mediaTimeUs presentation time to get timed event callback at + * @hide + */ + public void notifyAt(long mediaTimeUs) { } + + /** + * Sets the audio attributes for this MediaPlayer2. + * See {@link AudioAttributes} for how to build and configure an instance of this class. + * You must call this method before {@link #prepareAsync()} in order + * for the audio attributes to become effective thereafter. + * @param attributes a non-null set of audio attributes + * @throws IllegalArgumentException if the attributes are null or invalid. + */ + public abstract void setAudioAttributes(AudioAttributes attributes); + + /** + * Sets the player to be looping or non-looping. + * + * @param looping whether to loop or not + * @hide + */ + public void setLooping(boolean looping) { } + + /** + * Checks whether the MediaPlayer2 is looping or non-looping. + * + * @return true if the MediaPlayer2 is currently looping, false otherwise + * @hide + */ + public boolean isLooping() { + return false; + } + + /** + * Sets the volume on this player. + * This API is recommended for balancing the output of audio streams + * within an application. Unless you are writing an application to + * control user settings, this API should be used in preference to + * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of + * a particular type. Note that the passed volume values are raw scalars in range 0.0 to 1.0. + * UI controls should be scaled logarithmically. + * + * @param leftVolume left volume scalar + * @param rightVolume right volume scalar + */ + /* + * FIXME: Merge this into javadoc comment above when setVolume(float) is not @hide. + * The single parameter form below is preferred if the channel volumes don't need + * to be set independently. + */ + public abstract void setVolume(float leftVolume, float rightVolume); + + /** + * Similar, excepts sets volume of all channels to same value. + * @hide + */ + public void setVolume(float volume) { } + + /** + * Sets the audio session ID. + * + * @param sessionId the audio session ID. + * The audio session ID is a system wide unique identifier for the audio stream played by + * this MediaPlayer2 instance. + * The primary use of the audio session ID is to associate audio effects to a particular + * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect, + * this effect will be applied only to the audio content of media players within the same + * audio session and not to the output mix. + * When created, a MediaPlayer2 instance automatically generates its own audio session ID. + * However, it is possible to force this player to be part of an already existing audio session + * by calling this method. + * This method must be called before one of the overloaded <code> setDataSource </code> methods. + * @throws IllegalStateException if it is called in an invalid state + * @throws IllegalArgumentException if the sessionId is invalid. + */ + public abstract void setAudioSessionId(int sessionId); + + /** + * Returns the audio session ID. + * + * @return the audio session ID. {@see #setAudioSessionId(int)} + * Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was contructed. + */ + public abstract int getAudioSessionId(); + + /** + * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation + * effect which can be applied on any sound source that directs a certain amount of its + * energy to this effect. This amount is defined by setAuxEffectSendLevel(). + * See {@link #setAuxEffectSendLevel(float)}. + * <p>After creating an auxiliary effect (e.g. + * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with + * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method + * to attach the player to the effect. + * <p>To detach the effect from the player, call this method with a null effect id. + * <p>This method must be called after one of the overloaded <code> setDataSource </code> + * methods. + * @param effectId system wide unique id of the effect to attach + */ + public abstract void attachAuxEffect(int effectId); + + + /** + * Sets the send level of the player to the attached auxiliary effect. + * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0. + * <p>By default the send level is 0, so even if an effect is attached to the player + * this method must be called for the effect to be applied. + * <p>Note that the passed level value is a raw scalar. UI controls should be scaled + * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB, + * so an appropriate conversion from linear UI input x to level is: + * x == 0 -> level = 0 + * 0 < x <= R -> level = 10^(72*(x-R)/20/R) + * @param level send level scalar + */ + public abstract void setAuxEffectSendLevel(float level); + + /** + * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata. + * + * @see android.media.MediaPlayer2#getTrackInfo + */ + public abstract static class TrackInfo { + /** + * Gets the track type. + * @return TrackType which indicates if the track is video, audio, timed text. + */ + public abstract int getTrackType(); + + /** + * Gets the language code of the track. + * @return a language code in either way of ISO-639-1 or ISO-639-2. + * When the language is unknown or could not be determined, + * ISO-639-2 language code, "und", is returned. + */ + public abstract String getLanguage(); + + /** + * Gets the {@link MediaFormat} of the track. If the format is + * unknown or could not be determined, null is returned. + */ + public abstract MediaFormat getFormat(); + + public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; + public static final int MEDIA_TRACK_TYPE_VIDEO = 1; + public static final int MEDIA_TRACK_TYPE_AUDIO = 2; + + /** @hide */ + public static final int MEDIA_TRACK_TYPE_TIMEDTEXT = 3; + + public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; + public static final int MEDIA_TRACK_TYPE_METADATA = 5; + + @Override + public abstract String toString(); + }; + + /** + * Returns a List of track information. + * + * @return List of track info. The total number of tracks is the array length. + * Must be called again if an external timed text source has been added after + * addTimedTextSource method is called. + * @throws IllegalStateException if it is called in an invalid state. + */ + public abstract List<TrackInfo> getTrackInfo(); + + /* Do not change these values without updating their counterparts + * in include/media/stagefright/MediaDefs.h and media/libstagefright/MediaDefs.cpp! + */ + /** + * MIME type for SubRip (SRT) container. Used in addTimedTextSource APIs. + * @hide + */ + public static final String MEDIA_MIMETYPE_TEXT_SUBRIP = "application/x-subrip"; + + /** + * MIME type for WebVTT subtitle data. + * @hide + */ + 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"; + + /** + * MIME type for CEA-708 closed caption data. + * @hide + */ + public static final String MEDIA_MIMETYPE_TEXT_CEA_708 = "text/cea-708"; + + /** @hide */ + public void setSubtitleAnchor( + SubtitleController controller, + SubtitleController.Anchor anchor) { } + + /** @hide */ + @Override + public void onSubtitleTrackSelected(SubtitleTrack track) { } + + /** @hide */ + public void addSubtitleSource(InputStream is, MediaFormat format) { } + + /* TODO: Limit the total number of external timed text source to a reasonable number. + */ + /** + * Adds an external timed text source file. + * + * Currently supported format is SubRip with the file extension .srt, case insensitive. + * Note that a single external timed text source may contain multiple tracks in it. + * One can find the total number of available tracks using {@link #getTrackInfo()} to see what + * additional tracks become available after this method call. + * + * @param path The file path of external timed text source file. + * @param mimeType The mime type of the file. Must be one of the mime types listed above. + * @throws IOException if the file cannot be accessed or is corrupted. + * @throws IllegalArgumentException if the mimeType is not supported. + * @throws IllegalStateException if called in an invalid state. + * @hide + */ + public void addTimedTextSource(String path, String mimeType) throws IOException { } + + /** + * Adds an external timed text source file (Uri). + * + * Currently supported format is SubRip with the file extension .srt, case insensitive. + * Note that a single external timed text source may contain multiple tracks in it. + * One can find the total number of available tracks using {@link #getTrackInfo()} to see what + * additional tracks become available after this method call. + * + * @param context the Context to use when resolving the Uri + * @param uri the Content URI of the data you want to play + * @param mimeType The mime type of the file. Must be one of the mime types listed above. + * @throws IOException if the file cannot be accessed or is corrupted. + * @throws IllegalArgumentException if the mimeType is not supported. + * @throws IllegalStateException if called in an invalid state. + * @hide + */ + public void addTimedTextSource(Context context, Uri uri, String mimeType) throws IOException { } + + /** + * Adds an external timed text source file (FileDescriptor). + * + * It is the caller's responsibility to close the file descriptor. + * It is safe to do so as soon as this call returns. + * + * Currently supported format is SubRip. Note that a single external timed text source may + * contain multiple tracks in it. One can find the total number of available tracks + * using {@link #getTrackInfo()} to see what additional tracks become available + * after this method call. + * + * @param fd the FileDescriptor for the file you want to play + * @param mimeType The mime type of the file. Must be one of the mime types listed above. + * @throws IllegalArgumentException if the mimeType is not supported. + * @throws IllegalStateException if called in an invalid state. + * @hide + */ + public void addTimedTextSource(FileDescriptor fd, String mimeType) { } + + /** + * Adds an external timed text file (FileDescriptor). + * + * It is the caller's responsibility to close the file descriptor. + * It is safe to do so as soon as this call returns. + * + * Currently supported format is SubRip. Note that a single external timed text source may + * contain multiple tracks in it. One can find the total number of available tracks + * using {@link #getTrackInfo()} to see what additional tracks become available + * after this method call. + * + * @param fd the FileDescriptor for the file you want to play + * @param offset the offset into the file where the data to be played starts, in bytes + * @param length the length in bytes of the data to be played + * @param mime The mime type of the file. Must be one of the mime types listed above. + * @throws IllegalArgumentException if the mimeType is not supported. + * @throws IllegalStateException if called in an invalid state. + * @hide + */ + public abstract void addTimedTextSource(FileDescriptor fd, long offset, long length, String mime); + + /** + * Returns the index of the audio, video, or subtitle track currently selected for playback, + * The return value is an index into the array returned by {@link #getTrackInfo()}, and can + * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}. + * + * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO}, + * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or + * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE} + * @return index of the audio, video, or subtitle track currently selected for playback; + * a negative integer is returned when there is no selected track for {@code trackType} or + * when {@code trackType} is not one of audio, video, or subtitle. + * @throws IllegalStateException if called after {@link #close()} + * + * @see #getTrackInfo() + * @see #selectTrack(int) + * @see #deselectTrack(int) + */ + public abstract int getSelectedTrack(int trackType); + + /** + * Selects a track. + * <p> + * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception. + * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately. + * If a MediaPlayer2 is not in Started state, it just marks the track to be played. + * </p> + * <p> + * In any valid state, if it is called multiple times on the same type of track (ie. Video, + * Audio, Timed Text), the most recent one will be chosen. + * </p> + * <p> + * The first audio and video tracks are selected by default if available, even though + * this method is not called. However, no timed text track will be selected until + * this function is called. + * </p> + * <p> + * Currently, only timed text tracks or audio tracks can be selected via this method. + * In addition, the support for selecting an audio track at runtime is pretty limited + * in that an audio track can only be selected in the <em>Prepared</em> state. + * </p> + * @param index the index of the track to be selected. The valid range of the index + * is 0..total number of track - 1. The total number of tracks as well as the type of + * each individual track can be found by calling {@link #getTrackInfo()} method. + * @throws IllegalStateException if called in an invalid state. + * + * @see android.media.MediaPlayer2#getTrackInfo + */ + public abstract void selectTrack(int index); + + /** + * Deselect a track. + * <p> + * Currently, the track must be a timed text track and no audio or video tracks can be + * deselected. If the timed text track identified by index has not been + * selected before, it throws an exception. + * </p> + * @param index the index of the track to be deselected. The valid range of the index + * is 0..total number of tracks - 1. The total number of tracks as well as the type of + * each individual track can be found by calling {@link #getTrackInfo()} method. + * @throws IllegalStateException if called in an invalid state. + * + * @see android.media.MediaPlayer2#getTrackInfo + */ + public abstract void deselectTrack(int index); + + /** + * Releases the resources held by this {@code MediaPlayer2} object. + * + * It is considered good practice to call this method when you're + * done using the MediaPlayer2. In particular, whenever an Activity + * of an application is paused (its onPause() method is called), + * or stopped (its onStop() method is called), this method should be + * invoked to release the MediaPlayer2 object, unless the application + * has a special need to keep the object around. In addition to + * unnecessary resources (such as memory and instances of codecs) + * being held, failure to call this method immediately if a + * MediaPlayer2 object is no longer needed may also lead to + * continuous battery consumption for mobile devices, and playback + * failure for other applications if no multiple instances of the + * same codec are supported on a device. Even if multiple instances + * of the same codec are supported, some performance degradation + * may be expected when unnecessary multiple instances are used + * at the same time. + * + * {@code close()} may be safely called after a prior {@code close()}. + * This class implements the Java {@code AutoCloseable} interface and + * may be used with try-with-resources. + */ + @Override + public abstract void close(); + + /** @hide */ + public MediaTimeProvider getMediaTimeProvider() { + return null; + } + + /** + * Interface definition for callbacks to be invoked when the player has the corresponding + * events. + */ + public abstract static class EventCallback { + /** + * Called to update status in buffering a media source received through + * progressive downloading. The received buffering percentage + * indicates how much of the content has been buffered or played. + * For example a buffering update of 80 percent when half the content + * has already been played indicates that the next 30 percent of the + * content to play has been buffered. + * + * @param mp the MediaPlayer2 the update pertains to + * @param srcId the Id of this data source + * @param percent the percentage (0-100) of the content + * that has been buffered or played thus far + */ + public void onBufferingUpdate(MediaPlayer2 mp, long srcId, int percent) { } + + /** + * Called to indicate the video size + * + * The video size (width and height) could be 0 if there was no video, + * no display surface was set, or the value was not determined yet. + * + * @param mp the MediaPlayer2 associated with this callback + * @param srcId the Id of this data source + * @param width the width of the video + * @param height the height of the video + */ + public void onVideoSizeChanged(MediaPlayer2 mp, long srcId, int width, int height) { } + + /** + * Called to indicate an avaliable timed text + * + * @param mp the MediaPlayer2 associated with this callback + * @param srcId the Id of this data source + * @param text the timed text sample which contains the text + * needed to be displayed and the display format. + * @hide + */ + public void onTimedText(MediaPlayer2 mp, long srcId, TimedText text) { } + + /** + * Called to indicate avaliable timed metadata + * <p> + * This method will be called as timed metadata is extracted from the media, + * in the same order as it occurs in the media. The timing of this event is + * not controlled by the associated timestamp. + * <p> + * Currently only HTTP live streaming data URI's embedded with timed ID3 tags generates + * {@link TimedMetaData}. + * + * @see MediaPlayer2#selectTrack(int) + * @see MediaPlayer2.OnTimedMetaDataAvailableListener + * @see TimedMetaData + * + * @param mp the MediaPlayer2 associated with this callback + * @param srcId the Id of this data source + * @param data the timed metadata sample associated with this event + */ + public void onTimedMetaDataAvailable(MediaPlayer2 mp, long srcId, TimedMetaData data) { } + + /** + * Called to indicate an error. + * + * @param mp the MediaPlayer2 the error pertains to + * @param srcId the Id of this data source + * @param what the type of error that has occurred: + * <ul> + * <li>{@link #MEDIA_ERROR_UNKNOWN} + * </ul> + * @param extra an extra code, specific to the error. Typically + * implementation dependent. + * <ul> + * <li>{@link #MEDIA_ERROR_IO} + * <li>{@link #MEDIA_ERROR_MALFORMED} + * <li>{@link #MEDIA_ERROR_UNSUPPORTED} + * <li>{@link #MEDIA_ERROR_TIMED_OUT} + * <li><code>MEDIA_ERROR_SYSTEM (-2147483648)</code> - low-level system error. + * </ul> + */ + public void onError(MediaPlayer2 mp, long srcId, int what, int extra) { } + + /** + * Called to indicate an info or a warning. + * + * @param mp the MediaPlayer2 the info pertains to. + * @param srcId the Id of this data source + * @param what the type of info or warning. + * <ul> + * <li>{@link #MEDIA_INFO_UNKNOWN} + * <li>{@link #MEDIA_INFO_STARTED_AS_NEXT} + * <li>{@link #MEDIA_INFO_VIDEO_RENDERING_START} + * <li>{@link #MEDIA_INFO_AUDIO_RENDERING_START} + * <li>{@link #MEDIA_INFO_PLAYBACK_COMPLETE} + * <li>{@link #MEDIA_INFO_PLAYLIST_END} + * <li>{@link #MEDIA_INFO_PREPARED} + * <li>{@link #MEDIA_INFO_COMPLETE_CALL_PLAY} + * <li>{@link #MEDIA_INFO_COMPLETE_CALL_PAUSE} + * <li>{@link #MEDIA_INFO_COMPLETE_CALL_SEEK} + * <li>{@link #MEDIA_INFO_VIDEO_TRACK_LAGGING} + * <li>{@link #MEDIA_INFO_BUFFERING_START} + * <li>{@link #MEDIA_INFO_BUFFERING_END} + * <li><code>MEDIA_INFO_NETWORK_BANDWIDTH (703)</code> - + * bandwidth information is available (as <code>extra</code> kbps) + * <li>{@link #MEDIA_INFO_BAD_INTERLEAVING} + * <li>{@link #MEDIA_INFO_NOT_SEEKABLE} + * <li>{@link #MEDIA_INFO_METADATA_UPDATE} + * <li>{@link #MEDIA_INFO_UNSUPPORTED_SUBTITLE} + * <li>{@link #MEDIA_INFO_SUBTITLE_TIMED_OUT} + * </ul> + * @param extra an extra code, specific to the info. Typically + * implementation dependent. + */ + public void onInfo(MediaPlayer2 mp, long srcId, int what, int extra) { } + } + + /** + * Register a callback to be invoked when the media source is ready + * for playback. + * + * @param eventCallback the callback that will be run + * @param executor the executor through which the callback should be invoked + */ + public abstract void registerEventCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull EventCallback eventCallback); + + /** + * Unregisters an {@link EventCallback}. + * + * @param callback an {@link EventCallback} to unregister + */ + public abstract void unregisterEventCallback(EventCallback callback); + + /** + * Interface definition of a callback to be invoked when a + * track has data available. + * + * @hide + */ + public interface OnSubtitleDataListener + { + public void onSubtitleData(MediaPlayer2 mp, SubtitleData data); + } + + /** + * Register a callback to be invoked when a track has data available. + * + * @param listener the callback that will be run + * + * @hide + */ + public void setOnSubtitleDataListener(OnSubtitleDataListener listener) { } + + + /* Do not change these values without updating their counterparts + * in include/media/mediaplayer2.h! + */ + /** Unspecified media player error. + * @see android.media.MediaPlayer2.EventCallback.onError + */ + public static final int MEDIA_ERROR_UNKNOWN = 1; + + /** The video is streamed and its container is not valid for progressive + * playback i.e the video's index (e.g moov atom) is not at the start of the + * file. + * @see android.media.MediaPlayer2.EventCallback.onError + */ + public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200; + + /** File or network related operation errors. */ + public static final int MEDIA_ERROR_IO = -1004; + /** Bitstream is not conforming to the related coding standard or file spec. */ + public static final int MEDIA_ERROR_MALFORMED = -1007; + /** Bitstream is conforming to the related coding standard or file spec, but + * the media framework does not support the feature. */ + public static final int MEDIA_ERROR_UNSUPPORTED = -1010; + /** Some operation takes too long to complete, usually more than 3-5 seconds. */ + public static final int MEDIA_ERROR_TIMED_OUT = -110; + + /** Unspecified low-level system error. This value originated from UNKNOWN_ERROR in + * system/core/include/utils/Errors.h + * @see android.media.MediaPlayer2.EventCallback.onError + * @hide + */ + public static final int MEDIA_ERROR_SYSTEM = -2147483648; + + + /* Do not change these values without updating their counterparts + * in include/media/mediaplayer2.h! + */ + /** Unspecified media player info. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_UNKNOWN = 1; + + /** The player switched to this datas source because it is the + * next-to-be-played in the play list. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_STARTED_AS_NEXT = 2; + + /** The player just pushed the very first video frame for rendering. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; + + /** The player just rendered the very first audio sample. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_AUDIO_RENDERING_START = 4; + + /** The player just completed the playback of this data source. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_PLAYBACK_COMPLETE = 5; + + /** The player just completed the playback of the full play list. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_PLAYLIST_END = 6; + + /** The player just prepared a data source. + * This also serves as call completion notification for {@link #prepareAsync()}. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_PREPARED = 100; + + /** The player just completed a call {@link #play()}. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_COMPLETE_CALL_PLAY = 101; + + /** The player just completed a call {@link #pause()}. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_COMPLETE_CALL_PAUSE = 102; + + /** The player just completed a call {@link #seekTo(long, int)}. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_COMPLETE_CALL_SEEK = 103; + + /** The video is too complex for the decoder: it can't decode frames fast + * enough. Possibly only the audio plays fine at this stage. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; + + /** MediaPlayer2 is temporarily pausing playback internally in order to + * buffer more data. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_BUFFERING_START = 701; + + /** MediaPlayer2 is resuming playback after filling buffers. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_BUFFERING_END = 702; + + /** Estimated network bandwidth information (kbps) is available; currently this event fires + * simultaneously as {@link #MEDIA_INFO_BUFFERING_START} and {@link #MEDIA_INFO_BUFFERING_END} + * when playing network files. + * @see android.media.MediaPlayer2.EventCallback.onInfo + * @hide + */ + public static final int MEDIA_INFO_NETWORK_BANDWIDTH = 703; + + /** Bad interleaving means that a media has been improperly interleaved or + * not interleaved at all, e.g has all the video samples first then all the + * audio ones. Video is playing but a lot of disk seeks may be happening. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; + + /** The media cannot be seeked (e.g live stream) + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_NOT_SEEKABLE = 801; + + /** A new set of metadata is available. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_METADATA_UPDATE = 802; + + /** A new set of external-only metadata is available. Used by + * JAVA framework to avoid triggering track scanning. + * @hide + */ + public static final int MEDIA_INFO_EXTERNAL_METADATA_UPDATE = 803; + + /** Informs that audio is not playing. Note that playback of the video + * is not interrupted. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; + + /** Informs that video is not playing. Note that playback of the audio + * is not interrupted. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; + + /** Failed to handle timed text track properly. + * @see android.media.MediaPlayer2.EventCallback.onInfo + * + * {@hide} + */ + public static final int MEDIA_INFO_TIMED_TEXT_ERROR = 900; + + /** Subtitle track was not supported by the media framework. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901; + + /** Reading the subtitle track takes too long. + * @see android.media.MediaPlayer2.EventCallback.onInfo + */ + public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902; + + + // Modular DRM begin + + /** + * Interface definition of a callback to be invoked when the app + * can do DRM configuration (get/set properties) before the session + * is opened. This facilitates configuration of the properties, like + * 'securityLevel', which has to be set after DRM scheme creation but + * before the DRM session is opened. + * + * The only allowed DRM calls in this listener are {@code getDrmPropertyString} + * and {@code setDrmPropertyString}. + */ + public interface OnDrmConfigHelper + { + /** + * Called to give the app the opportunity to configure DRM before the session is created + * + * @param mp the {@code MediaPlayer2} associated with this callback + */ + public void onDrmConfig(MediaPlayer2 mp); + } + + /** + * Register a callback to be invoked for configuration of the DRM object before + * the session is created. + * The callback will be invoked synchronously during the execution + * of {@link #prepareDrm(UUID uuid)}. + * + * @param listener the callback that will be run + */ + public abstract void setOnDrmConfigHelper(OnDrmConfigHelper listener); + + /** + * Interface definition for callbacks to be invoked when the player has the corresponding + * DRM events. + */ + public abstract static class DrmEventCallback { + /** + * Called to indicate DRM info is available + * + * @param mp the {@code MediaPlayer2} associated with this callback + * @param drmInfo DRM info of the source including PSSH, and subset + * of crypto schemes supported by this device + */ + public void onDrmInfo(MediaPlayer2 mp, DrmInfo drmInfo) { } + + /** + * Called to notify the client that {@code prepareDrm} is finished and ready for key request/response. + * + * @param mp the {@code MediaPlayer2} associated with this callback + * @param status the result of DRM preparation which can be + * {@link #PREPARE_DRM_STATUS_SUCCESS}, + * {@link #PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR}, + * {@link #PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR}, or + * {@link #PREPARE_DRM_STATUS_PREPARATION_ERROR}. + */ + public void onDrmPrepared(MediaPlayer2 mp, @PrepareDrmStatusCode int status) { } + + } + + /** + * Register a callback to be invoked when the media source is ready + * for playback. + * + * @param eventCallback the callback that will be run + * @param executor the executor through which the callback should be invoked + */ + public abstract void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull DrmEventCallback eventCallback); + + /** + * Unregisters a {@link DrmEventCallback}. + * + * @param callback a {@link DrmEventCallback} to unregister + */ + public abstract void unregisterDrmEventCallback(DrmEventCallback callback); + + /** + * The status codes for {@link DrmEventCallback#onDrmPrepared} listener. + * <p> + * + * DRM preparation has succeeded. + */ + public static final int PREPARE_DRM_STATUS_SUCCESS = 0; + + /** + * The device required DRM provisioning but couldn't reach the provisioning server. + */ + public static final int PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR = 1; + + /** + * The device required DRM provisioning but the provisioning server denied the request. + */ + public static final int PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR = 2; + + /** + * The DRM preparation has failed . + */ + public static final int PREPARE_DRM_STATUS_PREPARATION_ERROR = 3; + + + /** @hide */ + @IntDef({ + PREPARE_DRM_STATUS_SUCCESS, + PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR, + PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR, + PREPARE_DRM_STATUS_PREPARATION_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PrepareDrmStatusCode {} + + /** + * Retrieves the DRM Info associated with the current source + * + * @throws IllegalStateException if called before being prepared + */ + public abstract DrmInfo getDrmInfo(); + + /** + * Prepares the DRM for the current source + * <p> + * If {@code OnDrmConfigHelper} is registered, it will be called during + * preparation to allow configuration of the DRM properties before opening the + * DRM session. Note that the callback is called synchronously in the thread that called + * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString} + * and {@code setDrmPropertyString} calls and refrain from any lengthy operation. + * <p> + * If the device has not been provisioned before, this call also provisions the device + * which involves accessing the provisioning server and can take a variable time to + * complete depending on the network connectivity. + * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking + * mode by launching the provisioning in the background and returning. The listener + * will be called when provisioning and preparation has finished. If a + * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning + * and preparation has finished, i.e., runs in blocking mode. + * <p> + * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM + * session being ready. The application should not make any assumption about its call + * sequence (e.g., before or after prepareDrm returns), or the thread context that will + * execute the listener (unless the listener is registered with a handler thread). + * <p> + * + * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved + * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}. + * + * @throws IllegalStateException if called before being prepared or the DRM was + * prepared already + * @throws UnsupportedSchemeException if the crypto scheme is not supported + * @throws ResourceBusyException if required DRM resources are in use + * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a + * network error + * @throws ProvisioningServerErrorException if provisioning is required but failed due to + * the request denied by the provisioning server + */ + public abstract void prepareDrm(@NonNull UUID uuid) + throws UnsupportedSchemeException, ResourceBusyException, + ProvisioningNetworkErrorException, ProvisioningServerErrorException; + + /** + * Releases the DRM session + * <p> + * The player has to have an active DRM session and be in stopped, or prepared + * state before this call is made. + * A {@code reset()} call will release the DRM session implicitly. + * + * @throws NoDrmSchemeException if there is no active DRM session to release + */ + public abstract void releaseDrm() throws NoDrmSchemeException; + + /** + * A key request/response exchange occurs between the app and a license server + * to obtain or release keys used to decrypt encrypted content. + * <p> + * getKeyRequest() is used to obtain an opaque key request byte array that is + * delivered to the license server. The opaque key request byte array is returned + * in KeyRequest.data. The recommended URL to deliver the key request to is + * returned in KeyRequest.defaultUrl. + * <p> + * After the app has received the key request response from the server, + * it should deliver to the response to the DRM engine plugin using the method + * {@link #provideKeyResponse}. + * + * @param keySetId is the key-set identifier of the offline keys being released when keyType is + * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when + * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. + * + * @param initData is the container-specific initialization data when the keyType is + * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is + * interpreted based on the mime type provided in the mimeType parameter. It could + * contain, for example, the content ID, key ID or other data obtained from the content + * metadata that is required in generating the key request. + * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null. + * + * @param mimeType identifies the mime type of the content + * + * @param keyType specifies the type of the request. The request may be to acquire + * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content + * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired + * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId. + * + * @param optionalParameters are included in the key request message to + * allow a client application to provide additional message parameters to the server. + * This may be {@code null} if no additional parameters are to be sent. + * + * @throws NoDrmSchemeException if there is no active DRM session + */ + @NonNull + public abstract MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData, + @Nullable String mimeType, @MediaDrm.KeyType int keyType, + @Nullable Map<String, String> optionalParameters) + throws NoDrmSchemeException; + + /** + * A key response is received from the license server by the app, then it is + * provided to the DRM engine plugin using provideKeyResponse. When the + * response is for an offline key request, a key-set identifier is returned that + * can be used to later restore the keys to a new session with the method + * {@ link # restoreKeys}. + * When the response is for a streaming or release request, null is returned. + * + * @param keySetId When the response is for a release request, keySetId identifies + * the saved key associated with the release request (i.e., the same keySetId + * passed to the earlier {@ link # getKeyRequest} call. It MUST be null when the + * response is for either streaming or offline key requests. + * + * @param response the byte array response from the server + * + * @throws NoDrmSchemeException if there is no active DRM session + * @throws DeniedByServerException if the response indicates that the + * server rejected the request + */ + public abstract byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response) + throws NoDrmSchemeException, DeniedByServerException; + + /** + * Restore persisted offline keys into a new session. keySetId identifies the + * keys to load, obtained from a prior call to {@link #provideKeyResponse}. + * + * @param keySetId identifies the saved key set to restore + */ + public abstract void restoreKeys(@NonNull byte[] keySetId) + throws NoDrmSchemeException; + + /** + * Read a DRM engine plugin String property value, given the property name string. + * <p> + * @param propertyName the property name + * + * Standard fields names are: + * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION}, + * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS} + */ + @NonNull + public abstract String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName) + throws NoDrmSchemeException; + + /** + * Set a DRM engine plugin String property value. + * <p> + * @param propertyName the property name + * @param value the property value + * + * Standard fields names are: + * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION}, + * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS} + */ + public abstract void setDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName, + @NonNull String value) + throws NoDrmSchemeException; + + /** + * Encapsulates the DRM properties of the source. + */ + public abstract static class DrmInfo { + /** + * Returns the PSSH info of the data source for each supported DRM scheme. + */ + public abstract Map<UUID, byte[]> getPssh(); + + /** + * Returns the intersection of the data source and the device DRM schemes. + * It effectively identifies the subset of the source's DRM schemes which + * are supported by the device too. + */ + public abstract List<UUID> getSupportedSchemes(); + }; // DrmInfo + + /** + * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm(). + * Extends MediaDrm.MediaDrmException + */ + public abstract static class NoDrmSchemeException extends MediaDrmException { + protected NoDrmSchemeException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Thrown when the device requires DRM provisioning but the provisioning attempt has + * failed due to a network error (Internet reachability, timeout, etc.). + * Extends MediaDrm.MediaDrmException + */ + public abstract static class ProvisioningNetworkErrorException extends MediaDrmException { + protected ProvisioningNetworkErrorException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Thrown when the device requires DRM provisioning but the provisioning attempt has + * failed due to the provisioning server denying the request. + * Extends MediaDrm.MediaDrmException + */ + public abstract static class ProvisioningServerErrorException extends MediaDrmException { + protected ProvisioningServerErrorException(String detailMessage) { + super(detailMessage); + } + } + + public static final class MetricsConstants { + private MetricsConstants() {} + + /** + * Key to extract the MIME type of the video track + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is a String. + */ + public static final String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime"; + + /** + * Key to extract the codec being used to decode the video track + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is a String. + */ + public static final String CODEC_VIDEO = "android.media.mediaplayer.video.codec"; + + /** + * Key to extract the width (in pixels) of the video track + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is an integer. + */ + public static final String WIDTH = "android.media.mediaplayer.width"; + + /** + * Key to extract the height (in pixels) of the video track + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is an integer. + */ + public static final String HEIGHT = "android.media.mediaplayer.height"; + + /** + * Key to extract the count of video frames played + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is an integer. + */ + public static final String FRAMES = "android.media.mediaplayer.frames"; + + /** + * Key to extract the count of video frames dropped + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is an integer. + */ + public static final String FRAMES_DROPPED = "android.media.mediaplayer.dropped"; + + /** + * Key to extract the MIME type of the audio track + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is a String. + */ + public static final String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime"; + + /** + * Key to extract the codec being used to decode the audio track + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is a String. + */ + public static final String CODEC_AUDIO = "android.media.mediaplayer.audio.codec"; + + /** + * Key to extract the duration (in milliseconds) of the + * media being played + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is a long. + */ + public static final String DURATION = "android.media.mediaplayer.durationMs"; + + /** + * Key to extract the playing time (in milliseconds) of the + * media being played + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is a long. + */ + public static final String PLAYING = "android.media.mediaplayer.playingMs"; + + /** + * Key to extract the count of errors encountered while + * playing the media + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is an integer. + */ + public static final String ERRORS = "android.media.mediaplayer.err"; + + /** + * Key to extract an (optional) error code detected while + * playing the media + * from the {@link MediaPlayer2#getMetrics} return value. + * The value is an integer. + */ + public static final String ERROR_CODE = "android.media.mediaplayer.errcode"; + + } +} diff --git a/media/java/android/media/MediaPlayer2Impl.java b/media/java/android/media/MediaPlayer2Impl.java new file mode 100644 index 000000000000..b805eb4482cd --- /dev/null +++ b/media/java/android/media/MediaPlayer2Impl.java @@ -0,0 +1,4928 @@ +/* + * Copyright 2018 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.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityThread; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.Process; +import android.os.PowerManager; +import android.os.SystemProperties; +import android.provider.Settings; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; +import android.util.Pair; +import android.util.ArrayMap; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.widget.VideoView; +import android.graphics.SurfaceTexture; +import android.media.SubtitleController.Anchor; +import android.media.SubtitleTrack.RenderingWidget; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; + +import dalvik.system.CloseGuard; + +import libcore.io.IoBridge; +import libcore.io.Streams; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.Runnable; +import java.lang.ref.WeakReference; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.Set; +import java.util.UUID; +import java.util.Vector; + + +/** + * MediaPlayer2 class can be used to control playback + * of audio/video files and streams. An example on how to use the methods in + * this class can be found in {@link android.widget.VideoView}. + * + * <p>Topics covered here are: + * <ol> + * <li><a href="#StateDiagram">State Diagram</a> + * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a> + * <li><a href="#Permissions">Permissions</a> + * <li><a href="#Callbacks">Register informational and error callbacks</a> + * </ol> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about how to use MediaPlayer2, read the + * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p> + * </div> + * + * <a name="StateDiagram"></a> + * <h3>State Diagram</h3> + * + * <p>Playback control of audio/video files and streams is managed as a state + * machine. The following diagram shows the life cycle and the states of a + * MediaPlayer2 object driven by the supported playback control operations. + * The ovals represent the states a MediaPlayer2 object may reside + * in. The arcs represent the playback control operations that drive the object + * state transition. There are two types of arcs. The arcs with a single arrow + * head represent synchronous method calls, while those with + * a double arrow head represent asynchronous method calls.</p> + * + * <p><img src="../../../images/mediaplayer_state_diagram.gif" + * alt="MediaPlayer State diagram" + * border="0" /></p> + * + * <p>From this state diagram, one can see that a MediaPlayer2 object has the + * following states:</p> + * <ul> + * <li>When a MediaPlayer2 object is just created using <code>new</code> or + * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after + * {@link #close()} is called, it is in the <em>End</em> state. Between these + * two states is the life cycle of the MediaPlayer2 object. + * <ul> + * <li>There is a subtle but important difference between a newly constructed + * MediaPlayer2 object and the MediaPlayer2 object after {@link #reset()} + * is called. It is a programming error to invoke methods such + * as {@link #getCurrentPosition()}, + * {@link #getDuration()}, {@link #getVideoHeight()}, + * {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)}, + * {@link #setLooping(boolean)}, + * {@link #setVolume(float, float)}, {@link #pause()}, {@link #play()}, + * {@link #seekTo(long, int)}, {@link #prepare()} or + * {@link #prepareAsync()} in the <em>Idle</em> state for both cases. If any of these + * methods is called right after a MediaPlayer2 object is constructed, + * the user supplied callback method OnErrorListener.onError() won't be + * called by the internal player engine and the object state remains + * unchanged; but if these methods are called right after {@link #reset()}, + * the user supplied callback method OnErrorListener.onError() will be + * invoked by the internal player engine and the object will be + * transfered to the <em>Error</em> state. </li> + * <li>It is also recommended that once + * a MediaPlayer2 object is no longer being used, call {@link #close()} immediately + * so that resources used by the internal player engine associated with the + * MediaPlayer2 object can be released immediately. Resource may include + * singleton resources such as hardware acceleration components and + * failure to call {@link #close()} may cause subsequent instances of + * MediaPlayer2 objects to fallback to software implementations or fail + * altogether. Once the MediaPlayer2 + * object is in the <em>End</em> state, it can no longer be used and + * there is no way to bring it back to any other state. </li> + * <li>Furthermore, + * the MediaPlayer2 objects created using <code>new</code> is in the + * <em>Idle</em> state. + * </li> + * </ul> + * </li> + * <li>In general, some playback control operation may fail due to various + * reasons, such as unsupported audio/video format, poorly interleaved + * audio/video, resolution too high, streaming timeout, and the like. + * Thus, error reporting and recovery is an important concern under + * these circumstances. Sometimes, due to programming errors, invoking a playback + * control operation in an invalid state may also occur. Under all these + * error conditions, the internal player engine invokes a user supplied + * EventCallback.onError() method if an EventCallback has been + * registered beforehand via + * {@link #registerEventCallback(Executor, EventCallback)}. + * <ul> + * <li>It is important to note that once an error occurs, the + * MediaPlayer2 object enters the <em>Error</em> state (except as noted + * above), even if an error listener has not been registered by the application.</li> + * <li>In order to reuse a MediaPlayer2 object that is in the <em> + * Error</em> state and recover from the error, + * {@link #reset()} can be called to restore the object to its <em>Idle</em> + * state.</li> + * <li>It is good programming practice to have your application + * register a OnErrorListener to look out for error notifications from + * the internal player engine.</li> + * <li>IllegalStateException is + * thrown to prevent programming errors such as calling {@link #prepare()}, + * {@link #prepareAsync()}, {@link #setDataSource(DataSourceDesc)}, or + * {@code setPlaylist} methods in an invalid state. </li> + * </ul> + * </li> + * <li>Calling + * {@link #setDataSource(DataSourceDesc)}, or + * {@code setPlaylist} transfers a + * MediaPlayer2 object in the <em>Idle</em> state to the + * <em>Initialized</em> state. + * <ul> + * <li>An IllegalStateException is thrown if + * setDataSource() or setPlaylist() is called in any other state.</li> + * <li>It is good programming + * practice to always look out for <code>IllegalArgumentException</code> + * and <code>IOException</code> that may be thrown from + * <code>setDataSource</code> and <code>setPlaylist</code> methods.</li> + * </ul> + * </li> + * <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state + * before playback can be started. + * <ul> + * <li>There are two ways (synchronous vs. + * asynchronous) that the <em>Prepared</em> state can be reached: + * either a call to {@link #prepare()} (synchronous) which + * transfers the object to the <em>Prepared</em> state once the method call + * returns, or a call to {@link #prepareAsync()} (asynchronous) which + * first transfers the object to the <em>Preparing</em> state after the + * call returns (which occurs almost right way) while the internal + * player engine continues working on the rest of preparation work + * until the preparation work completes. When the preparation completes or when {@link #prepare()} call returns, + * the internal player engine then calls a user supplied callback method, + * onPrepared() of the EventCallback interface, if an + * EventCallback is registered beforehand via {@link + * #registerEventCallback(Executor, EventCallback)}.</li> + * <li>It is important to note that + * the <em>Preparing</em> state is a transient state, and the behavior + * of calling any method with side effect while a MediaPlayer2 object is + * in the <em>Preparing</em> state is undefined.</li> + * <li>An IllegalStateException is + * thrown if {@link #prepare()} or {@link #prepareAsync()} is called in + * any other state.</li> + * <li>While in the <em>Prepared</em> state, properties + * such as audio/sound volume, screenOnWhilePlaying, looping can be + * adjusted by invoking the corresponding set methods.</li> + * </ul> + * </li> + * <li>To start the playback, {@link #play()} must be called. After + * {@link #play()} returns successfully, the MediaPlayer2 object is in the + * <em>Started</em> state. {@link #isPlaying()} can be called to test + * whether the MediaPlayer2 object is in the <em>Started</em> state. + * <ul> + * <li>While in the <em>Started</em> state, the internal player engine calls + * a user supplied EventCallback.onBufferingUpdate() callback + * method if an EventCallback has been registered beforehand + * via {@link #registerEventCallback(Executor, EventCallback)}. + * This callback allows applications to keep track of the buffering status + * while streaming audio/video.</li> + * <li>Calling {@link #play()} has not effect + * on a MediaPlayer2 object that is already in the <em>Started</em> state.</li> + * </ul> + * </li> + * <li>Playback can be paused and stopped, and the current playback position + * can be adjusted. Playback can be paused via {@link #pause()}. When the call to + * {@link #pause()} returns, the MediaPlayer2 object enters the + * <em>Paused</em> state. Note that the transition from the <em>Started</em> + * state to the <em>Paused</em> state and vice versa happens + * asynchronously in the player engine. It may take some time before + * the state is updated in calls to {@link #isPlaying()}, and it can be + * a number of seconds in the case of streamed content. + * <ul> + * <li>Calling {@link #play()} to resume playback for a paused + * MediaPlayer2 object, and the resumed playback + * position is the same as where it was paused. When the call to + * {@link #play()} returns, the paused MediaPlayer2 object goes back to + * the <em>Started</em> state.</li> + * <li>Calling {@link #pause()} has no effect on + * a MediaPlayer2 object that is already in the <em>Paused</em> state.</li> + * </ul> + * </li> + * <li>The playback position can be adjusted with a call to + * {@link #seekTo(long, int)}. + * <ul> + * <li>Although the asynchronuous {@link #seekTo(long, int)} + * call returns right away, the actual seek operation may take a while to + * finish, especially for audio/video being streamed. When the actual + * seek operation completes, the internal player engine calls a user + * supplied EventCallback.onSeekComplete() if an EventCallback + * has been registered beforehand via + * {@link #registerEventCallback(Executor, EventCallback)}.</li> + * <li>Please + * note that {@link #seekTo(long, int)} can also be called in the other states, + * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted + * </em> state. When {@link #seekTo(long, int)} is called in those states, + * one video frame will be displayed if the stream has video and the requested + * position is valid. + * </li> + * <li>Furthermore, the actual current playback position + * can be retrieved with a call to {@link #getCurrentPosition()}, which + * is helpful for applications such as a Music player that need to keep + * track of the playback progress.</li> + * </ul> + * </li> + * <li>When the playback reaches the end of stream, the playback completes. + * <ul> + * <li>If the looping mode was being set to <var>true</var>with + * {@link #setLooping(boolean)}, the MediaPlayer2 object shall remain in + * the <em>Started</em> state.</li> + * <li>If the looping mode was set to <var>false + * </var>, the player engine calls a user supplied callback method, + * EventCallback.onCompletion(), if an EventCallback is registered + * beforehand via {@link #registerEventCallback(Executor, EventCallback)}. + * The invoke of the callback signals that the object is now in the <em> + * PlaybackCompleted</em> state.</li> + * <li>While in the <em>PlaybackCompleted</em> + * state, calling {@link #play()} can restart the playback from the + * beginning of the audio/video source.</li> + * </ul> + * + * + * <a name="Valid_and_Invalid_States"></a> + * <h3>Valid and invalid states</h3> + * + * <table border="0" cellspacing="0" cellpadding="0"> + * <tr><td>Method Name </p></td> + * <td>Valid Sates </p></td> + * <td>Invalid States </p></td> + * <td>Comments </p></td></tr> + * <tr><td>attachAuxEffect </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td> + * <td>{Idle, Error} </p></td> + * <td>This method must be called after setDataSource or setPlaylist. + * Calling it does not change the object state. </p></td></tr> + * <tr><td>getAudioSessionId </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>getCurrentPosition </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted} </p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getDuration </p></td> + * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td> + * <td>{Idle, Initialized, Error} </p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getVideoHeight </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change the + * state. Calling this method in an invalid state transfers the object + * to the <em>Error</em> state. </p></td></tr> + * <tr><td>getVideoWidth </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>isPlaying </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>pause </p></td> + * <td>{Started, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Paused</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>prepare </p></td> + * <td>{Initialized, Stopped} </p></td> + * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Prepared</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>prepareAsync </p></td> + * <td>{Initialized, Stopped} </p></td> + * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Preparing</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>release </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>After {@link #close()}, the object is no longer available. </p></td></tr> + * <tr><td>reset </p></td> + * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped, + * PlaybackCompleted, Error}</p></td> + * <td>{}</p></td> + * <td>After {@link #reset()}, the object is like being just created.</p></td></tr> + * <tr><td>seekTo </p></td> + * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td> + * <td>{Idle, Initialized, Stopped, Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an invalid state transfers the + * object to the <em>Error</em> state. </p></td></tr> + * <tr><td>setAudioAttributes </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method does not change the state. In order for the + * target audio attributes type to become effective, this method must be called before + * prepare() or prepareAsync().</p></td></tr> + * <tr><td>setAudioSessionId </p></td> + * <td>{Idle} </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, + * Error} </p></td> + * <td>This method must be called in idle state as the audio session ID must be known before + * calling setDataSource or setPlaylist. Calling it does not change the object + * state. </p></td></tr> + * <tr><td>setAudioStreamType (deprecated)</p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method does not change the state. In order for the + * target audio stream type to become effective, this method must be called before + * prepare() or prepareAsync().</p></td></tr> + * <tr><td>setAuxEffectSendLevel </p></td> + * <td>any</p></td> + * <td>{} </p></td> + * <td>Calling this method does not change the object state. </p></td></tr> + * <tr><td>setDataSource </p></td> + * <td>{Idle} </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, + * Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Initialized</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>setPlaylist </p></td> + * <td>{Idle} </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, + * Error} </p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Initialized</em> state. Calling this method in an + * invalid state throws an IllegalStateException.</p></td></tr> + * <tr><td>setDisplay </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setSurface </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setVideoScalingMode </p></td> + * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td> + * <td>{Idle, Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * <tr><td>setLooping </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method in a valid state does not change + * the state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>isLooping </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>registerDrmEventCallback </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>registerEventCallback </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setPlaybackParams</p></td> + * <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td> + * <td>{Idle, Stopped} </p></td> + * <td>This method will change state in some cases, depending on when it's called. + * </p></td></tr> + * <tr><td>setScreenOnWhilePlaying</></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> + * <tr><td>setVolume </p></td> + * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, + * PlaybackCompleted}</p></td> + * <td>{Error}</p></td> + * <td>Successful invoke of this method does not change the state. + * <tr><td>setWakeMode </p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state.</p></td></tr> + * <tr><td>start </p></td> + * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Stopped, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Started</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>stop </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method in a valid state transfers the + * object to the <em>Stopped</em> state. Calling this method in an + * invalid state transfers the object to the <em>Error</em> state.</p></td></tr> + * <tr><td>getTrackInfo </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * <tr><td>addTimedTextSource </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * <tr><td>selectTrack </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * <tr><td>deselectTrack </p></td> + * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td> + * <td>{Idle, Initialized, Error}</p></td> + * <td>Successful invoke of this method does not change the state.</p></td></tr> + * + * </table> + * + * <a name="Permissions"></a> + * <h3>Permissions</h3> + * <p>One may need to declare a corresponding WAKE_LOCK permission {@link + * android.R.styleable#AndroidManifestUsesPermission <uses-permission>} + * element. + * + * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission + * when used with network-based content. + * + * <a name="Callbacks"></a> + * <h3>Callbacks</h3> + * <p>Applications may want to register for informational and error + * events in order to be informed of some internal state update and + * possible runtime errors during playback or streaming. Registration for + * these events is done by properly setting the appropriate listeners (via calls + * to + * {@link #registerEventCallback(Executor, EventCallback)}, + * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}). + * In order to receive the respective callback + * associated with these listeners, applications are required to create + * MediaPlayer2 objects on a thread with its own Looper running (main UI + * thread by default has a Looper running). + * + * @hide + */ +public final class MediaPlayer2Impl extends MediaPlayer2 { + static { + System.loadLibrary("media2_jni"); + native_init(); + } + + private final static String TAG = "MediaPlayer2Impl"; + + private long mNativeContext; // accessed by native methods + private long mNativeSurfaceTexture; // accessed by native methods + private int mListenerContext; // accessed by native methods + private SurfaceHolder mSurfaceHolder; + private EventHandler mEventHandler; + private PowerManager.WakeLock mWakeLock = null; + private boolean mScreenOnWhilePlaying; + private boolean mStayAwake; + private int mStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE; + private int mUsage = -1; + private boolean mBypassInterruptionPolicy; + private final CloseGuard mGuard = CloseGuard.get(); + + private List<DataSourceDesc> mPlaylist; + private int mPLCurrentIndex = 0; + private int mPLNextIndex = -1; + private int mLoopingMode = LOOPING_MODE_NONE; + + // Modular DRM + private UUID mDrmUUID; + private final Object mDrmLock = new Object(); + private DrmInfoImpl mDrmInfoImpl; + private MediaDrm mDrmObj; + private byte[] mDrmSessionId; + private boolean mDrmInfoResolved; + private boolean mActiveDrmScheme; + private boolean mDrmConfigAllowed; + private boolean mDrmProvisioningInProgress; + private boolean mPrepareDrmInProgress; + private ProvisioningThread mDrmProvisioningThread; + + /** + * Default constructor. + * <p>When done with the MediaPlayer2Impl, you should call {@link #close()}, + * to free the resources. If not released, too many MediaPlayer2Impl instances may + * result in an exception.</p> + */ + public MediaPlayer2Impl() { + Looper looper; + if ((looper = Looper.myLooper()) != null) { + mEventHandler = new EventHandler(this, looper); + } else if ((looper = Looper.getMainLooper()) != null) { + mEventHandler = new EventHandler(this, looper); + } else { + mEventHandler = null; + } + + mTimeProvider = new TimeProvider(this); + mOpenSubtitleSources = new Vector<InputStream>(); + mGuard.open("close"); + + /* Native setup requires a weak reference to our object. + * It's easier to create it here than in C++. + */ + native_setup(new WeakReference<MediaPlayer2Impl>(this)); + } + + /* + * Update the MediaPlayer2Impl SurfaceTexture. + * Call after setting a new display surface. + */ + private native void _setVideoSurface(Surface surface); + + /* Do not change these values (starting with INVOKE_ID) without updating + * their counterparts in include/media/mediaplayer2.h! + */ + private static final int INVOKE_ID_GET_TRACK_INFO = 1; + private static final int INVOKE_ID_ADD_EXTERNAL_SOURCE = 2; + private static final int INVOKE_ID_ADD_EXTERNAL_SOURCE_FD = 3; + private static final int INVOKE_ID_SELECT_TRACK = 4; + private static final int INVOKE_ID_DESELECT_TRACK = 5; + private static final int INVOKE_ID_SET_VIDEO_SCALE_MODE = 6; + private static final int INVOKE_ID_GET_SELECTED_TRACK = 7; + + /** + * Create a request parcel which can be routed to the native media + * player using {@link #invoke(Parcel, Parcel)}. The Parcel + * returned has the proper InterfaceToken set. The caller should + * not overwrite that token, i.e it can only append data to the + * Parcel. + * + * @return A parcel suitable to hold a request for the native + * player. + * {@hide} + */ + @Override + public Parcel newRequest() { + Parcel parcel = Parcel.obtain(); + return parcel; + } + + /** + * Invoke a generic method on the native player using opaque + * parcels for the request and reply. Both payloads' format is a + * convention between the java caller and the native player. + * Must be called after setDataSource or setPlaylist to make sure a native player + * exists. On failure, a RuntimeException is thrown. + * + * @param request Parcel with the data for the extension. The + * caller must use {@link #newRequest()} to get one. + * + * @param reply Output parcel with the data returned by the + * native player. + * {@hide} + */ + @Override + public void invoke(Parcel request, Parcel reply) { + int retcode = native_invoke(request, reply); + reply.setDataPosition(0); + if (retcode != 0) { + throw new RuntimeException("failure code: " + retcode); + } + } + + /** + * Sets the {@link SurfaceHolder} to use for displaying the video + * portion of the media. + * + * Either a surface holder or surface must be set if a display or video sink + * is needed. Not calling this method or {@link #setSurface(Surface)} + * when playing back a video will result in only the audio track being played. + * A null surface holder or surface will result in only the audio track being + * played. + * + * @param sh the SurfaceHolder to use for video display + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released. + * @hide + */ + @Override + public void setDisplay(SurfaceHolder sh) { + mSurfaceHolder = sh; + Surface surface; + if (sh != null) { + surface = sh.getSurface(); + } else { + surface = null; + } + _setVideoSurface(surface); + updateSurfaceScreenOn(); + } + + /** + * Sets the {@link Surface} to be used as the sink for the video portion of + * the media. This is similar to {@link #setDisplay(SurfaceHolder)}, but + * does not support {@link #setScreenOnWhilePlaying(boolean)}. Setting a + * Surface will un-set any Surface or SurfaceHolder that was previously set. + * A null surface will result in only the audio track being played. + * + * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps + * returned from {@link SurfaceTexture#getTimestamp()} will have an + * unspecified zero point. These timestamps cannot be directly compared + * between different media sources, different instances of the same media + * source, or multiple runs of the same program. The timestamp is normally + * monotonically increasing and is unaffected by time-of-day adjustments, + * but it is reset when the position is set. + * + * @param surface The {@link Surface} to be used for the video portion of + * the media. + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released. + */ + @Override + public void setSurface(Surface surface) { + if (mScreenOnWhilePlaying && surface != null) { + Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective for Surface"); + } + mSurfaceHolder = null; + _setVideoSurface(surface); + updateSurfaceScreenOn(); + } + + /** + * Sets video scaling mode. To make the target video scaling mode + * effective during playback, this method must be called after + * data source is set. If not called, the default video + * scaling mode is {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}. + * + * <p> The supported video scaling modes are: + * <ul> + * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT} + * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING} + * </ul> + * + * @param mode target video scaling mode. Must be one of the supported + * video scaling modes; otherwise, IllegalArgumentException will be thrown. + * + * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT + * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING + * @hide + */ + @Override + public void setVideoScalingMode(int mode) { + if (!isVideoScalingModeSupported(mode)) { + final String msg = "Scaling mode " + mode + " is not supported"; + throw new IllegalArgumentException(msg); + } + Parcel request = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + try { + request.writeInt(INVOKE_ID_SET_VIDEO_SCALE_MODE); + request.writeInt(mode); + invoke(request, reply); + } finally { + request.recycle(); + reply.recycle(); + } + } + + /** + * Discards all pending commands. + */ + @Override + public void clearPendingCommands() { + } + + /** + * Sets the data source as described by a DataSourceDesc. + * + * @param dsd the descriptor of data source you want to play + * @throws IllegalStateException if it is called in an invalid state + * @throws NullPointerException if dsd is null + */ + @Override + public void setDataSource(@NonNull DataSourceDesc dsd) throws IOException { + Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null"); + mPlaylist = Collections.synchronizedList(new ArrayList<DataSourceDesc>(1)); + mPlaylist.add(dsd); + mPLCurrentIndex = 0; + setDataSourcePriv(dsd); + } + + /** + * Gets the current data source as described by a DataSourceDesc. + * + * @return the current DataSourceDesc + */ + @Override + public DataSourceDesc getCurrentDataSource() { + if (mPlaylist == null) { + return null; + } + return mPlaylist.get(mPLCurrentIndex); + } + + /** + * Sets the play list. + * + * If startIndex falls outside play list range, it will be clamped to the nearest index + * in the play list. + * + * @param pl the play list of data source you want to play + * @param startIndex the index of the DataSourceDesc in the play list you want to play first + * @throws IllegalStateException if it is called in an invalid state + * @throws IllegalArgumentException if pl is null or empty, or pl contains null DataSourceDesc + */ + @Override + public void setPlaylist(@NonNull List<DataSourceDesc> pl, int startIndex) + throws IOException { + if (pl == null || pl.size() == 0) { + throw new IllegalArgumentException("play list cannot be null or empty."); + } + HashSet ids = new HashSet(pl.size()); + for (DataSourceDesc dsd : pl) { + if (dsd == null) { + throw new IllegalArgumentException("DataSourceDesc in play list cannot be null."); + } + if (ids.add(dsd.getId()) == false) { + throw new IllegalArgumentException("DataSourceDesc Id in play list should be unique."); + } + } + + if (startIndex < 0) { + startIndex = 0; + } else if (startIndex >= pl.size()) { + startIndex = pl.size() - 1; + } + + mPlaylist = Collections.synchronizedList(new ArrayList(pl)); + mPLCurrentIndex = startIndex; + setDataSourcePriv(mPlaylist.get(startIndex)); + // TODO: handle the preparation of next source in the play list. + // It should be processed after current source is prepared. + } + + /** + * Gets a copy of the play list. + * + * @return a copy of the play list used by {@link MediaPlayer2} + */ + @Override + public List<DataSourceDesc> getPlaylist() { + if (mPlaylist == null) { + return null; + } + return new ArrayList(mPlaylist); + } + + /** + * Sets the index of current DataSourceDesc in the play list to be played. + * + * @param index the index of DataSourceDesc in the play list you want to play + * @throws IllegalArgumentException if the play list is null + * @throws NullPointerException if index is outside play list range + */ + @Override + public void setCurrentPlaylistItem(int index) { + if (mPlaylist == null) { + throw new IllegalArgumentException("play list has not been set yet."); + } + if (index < 0 || index >= mPlaylist.size()) { + throw new IndexOutOfBoundsException("index is out of play list range."); + } + + if (index == mPLCurrentIndex) { + return; + } + + // TODO: in playing state, stop current source and start to play source of index. + mPLCurrentIndex = index; + } + + /** + * Sets the index of next-to-be-played DataSourceDesc in the play list. + * + * @param index the index of next-to-be-played DataSourceDesc in the play list + * @throws IllegalArgumentException if the play list is null + * @throws NullPointerException if index is outside play list range + */ + @Override + public void setNextPlaylistItem(int index) { + if (mPlaylist == null) { + throw new IllegalArgumentException("play list has not been set yet."); + } + if (index < 0 || index >= mPlaylist.size()) { + throw new IndexOutOfBoundsException("index is out of play list range."); + } + + if (index == mPLNextIndex) { + return; + } + + // TODO: prepare the new next-to-be-played DataSourceDesc + mPLNextIndex = index; + } + + /** + * Gets the current index of play list. + * + * @return the index of the current DataSourceDesc in the play list + */ + @Override + public int getCurrentPlaylistItemIndex() { + return mPLCurrentIndex; + } + + /** + * Sets the looping mode of the play list. + * The mode shall be one of {@link #LOOPING_MODE_NONE}, {@link #LOOPING_MODE_FULL}, + * {@link #LOOPING_MODE_SINGLE}, {@link #LOOPING_MODE_SHUFFLE}. + * + * @param mode the mode in which the play list will be played + * @throws IllegalArgumentException if mode is not supported + */ + @Override + public void setLoopingMode(@LoopingMode int mode) { + if (mode != LOOPING_MODE_NONE + && mode != LOOPING_MODE_FULL + && mode != LOOPING_MODE_SINGLE + && mode != LOOPING_MODE_SHUFFLE) { + throw new IllegalArgumentException("mode is not supported."); + } + mLoopingMode = mode; + if (mPlaylist == null) { + return; + } + + // TODO: handle the new mode if necessary. + } + + /** + * Gets the looping mode of play list. + * + * @return the looping mode of the play list + */ + @Override + public int getLoopingMode() { + return mPLCurrentIndex; + } + + /** + * Moves the DataSourceDesc at indexFrom in the play list to indexTo. + * + * @throws IllegalArgumentException if the play list is null + * @throws IndexOutOfBoundsException if indexFrom or indexTo is outside play list range + */ + @Override + public void movePlaylistItem(int indexFrom, int indexTo) { + if (mPlaylist == null) { + throw new IllegalArgumentException("play list has not been set yet."); + } + // TODO: move the DataSourceDesc from indexFrom to indexTo. + } + + /** + * Removes the DataSourceDesc at index in the play list. + * + * If index is same as the current index of the play list, current DataSourceDesc + * will be stopped and playback moves to next source in the list. + * + * @return the removed DataSourceDesc at index in the play list + * @throws IllegalArgumentException if the play list is null + * @throws IndexOutOfBoundsException if index is outside play list range + */ + @Override + public DataSourceDesc removePlaylistItem(int index) { + if (mPlaylist == null) { + throw new IllegalArgumentException("play list has not been set yet."); + } + + DataSourceDesc oldDsd = mPlaylist.remove(index); + // TODO: if index == mPLCurrentIndex, stop current source and move to next one. + // if index == mPLNextIndex, prepare the new next-to-be-played source. + return oldDsd; + } + + /** + * Inserts the DataSourceDesc to the play list at position index. + * + * This will not change the DataSourceDesc currently being played. + * If index is less than or equal to the current index of the play list, + * the current index of the play list will be incremented correspondingly. + * + * @param index the index you want to add dsd to the play list + * @param dsd the descriptor of data source you want to add to the play list + * @throws IndexOutOfBoundsException if index is outside play list range + * @throws NullPointerException if dsd is null + */ + @Override + public void addPlaylistItem(int index, DataSourceDesc dsd) { + Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null"); + + if (mPlaylist == null) { + if (index == 0) { + mPlaylist = Collections.synchronizedList(new ArrayList<DataSourceDesc>()); + mPlaylist.add(dsd); + mPLCurrentIndex = 0; + return; + } + throw new IllegalArgumentException("index should be 0 for first DataSourceDesc."); + } + + long id = dsd.getId(); + for (DataSourceDesc pldsd : mPlaylist) { + if (id == pldsd.getId()) { + throw new IllegalArgumentException("Id of dsd already exists in the play list."); + } + } + + mPlaylist.add(index, dsd); + if (index <= mPLCurrentIndex) { + ++mPLCurrentIndex; + } + } + + /** + * replaces the DataSourceDesc at index in the play list with given dsd. + * + * When index is same as the current index of the play list, the current source + * will be stopped and the new source will be played, except that if new + * and old source only differ on end position and current media position is + * smaller then the new end position. + * + * This will not change the DataSourceDesc currently being played. + * If index is less than or equal to the current index of the play list, + * the current index of the play list will be incremented correspondingly. + * + * @param index the index you want to add dsd to the play list + * @param dsd the descriptor of data source you want to add to the play list + * @throws IndexOutOfBoundsException if index is outside play list range + * @throws NullPointerException if dsd is null + */ + @Override + public DataSourceDesc editPlaylistItem(int index, DataSourceDesc dsd) { + Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null"); + Preconditions.checkNotNull(mPlaylist, "the play list cannot be null"); + + long id = dsd.getId(); + for (int i = 0; i < mPlaylist.size(); ++i) { + if (i == index) { + continue; + } + if (id == mPlaylist.get(i).getId()) { + throw new IllegalArgumentException("Id of dsd already exists in the play list."); + } + } + + // TODO: if needed, stop playback of current source, and start new dsd. + DataSourceDesc oldDsd = mPlaylist.set(index, dsd); + return mPlaylist.set(index, dsd); + } + + private void setDataSourcePriv(@NonNull DataSourceDesc dsd) throws IOException { + Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null"); + + switch (dsd.getType()) { + case DataSourceDesc.TYPE_CALLBACK: + setDataSourcePriv(dsd.getId(), + dsd.getMedia2DataSource()); + break; + + case DataSourceDesc.TYPE_FD: + setDataSourcePriv(dsd.getId(), + dsd.getFileDescriptor(), + dsd.getFileDescriptorOffset(), + dsd.getFileDescriptorLength()); + break; + + case DataSourceDesc.TYPE_URI: + setDataSourcePriv(dsd.getId(), + dsd.getUriContext(), + dsd.getUri(), + dsd.getUriHeaders(), + dsd.getUriCookies()); + break; + + default: + break; + } + } + + /** + * To provide cookies for the subsequent HTTP requests, you can install your own default cookie + * handler and use other variants of setDataSource APIs instead. Alternatively, you can use + * this API to pass the cookies as a list of HttpCookie. If the app has not installed + * a CookieHandler already, this API creates a CookieManager and populates its CookieStore with + * the provided cookies. If the app has installed its own handler already, this API requires the + * handler to be of CookieManager type such that the API can update the manager’s CookieStore. + * + * <p><strong>Note</strong> that the cross domain redirection is allowed by default, + * but that can be changed with key/value pairs through the headers parameter with + * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to + * disallow or allow cross domain redirection. + * + * @throws IllegalArgumentException if cookies are provided and the installed handler is not + * a CookieManager + * @throws IllegalStateException if it is called in an invalid state + * @throws NullPointerException if context or uri is null + * @throws IOException if uri has a file scheme and an I/O error occurs + */ + private void setDataSourcePriv(long srcId, @NonNull Context context, @NonNull Uri uri, + @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) + throws IOException { + if (context == null) { + throw new NullPointerException("context param can not be null."); + } + + if (uri == null) { + throw new NullPointerException("uri param can not be null."); + } + + if (cookies != null) { + CookieHandler cookieHandler = CookieHandler.getDefault(); + if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) { + throw new IllegalArgumentException("The cookie handler has to be of CookieManager " + + "type when cookies are provided."); + } + } + + // The context and URI usually belong to the calling user. Get a resolver for that user + // and strip out the userId from the URI if present. + final ContentResolver resolver = context.getContentResolver(); + final String scheme = uri.getScheme(); + final String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority()); + if (ContentResolver.SCHEME_FILE.equals(scheme)) { + setDataSourcePriv(srcId, uri.getPath(), null, null); + return; + } else if (ContentResolver.SCHEME_CONTENT.equals(scheme) + && Settings.AUTHORITY.equals(authority)) { + // Try cached ringtone first since the actual provider may not be + // encryption aware, or it may be stored on CE media storage + final int type = RingtoneManager.getDefaultType(uri); + final Uri cacheUri = RingtoneManager.getCacheForType(type, context.getUserId()); + final Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, type); + if (attemptDataSource(srcId, resolver, cacheUri)) { + return; + } else if (attemptDataSource(srcId, resolver, actualUri)) { + return; + } else { + setDataSourcePriv(srcId, uri.toString(), headers, cookies); + } + } else { + // Try requested Uri locally first, or fallback to media server + if (attemptDataSource(srcId, resolver, uri)) { + return; + } else { + setDataSourcePriv(srcId, uri.toString(), headers, cookies); + } + } + } + + private boolean attemptDataSource(long srcId, ContentResolver resolver, Uri uri) { + try (AssetFileDescriptor afd = resolver.openAssetFileDescriptor(uri, "r")) { + if (afd.getDeclaredLength() < 0) { + setDataSourcePriv(srcId, afd.getFileDescriptor(), 0, DataSourceDesc.LONG_MAX); + } else { + setDataSourcePriv(srcId, + afd.getFileDescriptor(), + afd.getStartOffset(), + afd.getDeclaredLength()); + } + return true; + } catch (NullPointerException | SecurityException | IOException ex) { + Log.w(TAG, "Couldn't open " + uri + ": " + ex); + return false; + } + } + + private void setDataSourcePriv( + long srcId, String path, Map<String, String> headers, List<HttpCookie> cookies) + throws IOException, IllegalArgumentException, SecurityException, IllegalStateException + { + String[] keys = null; + String[] values = null; + + if (headers != null) { + keys = new String[headers.size()]; + values = new String[headers.size()]; + + int i = 0; + for (Map.Entry<String, String> entry: headers.entrySet()) { + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + ++i; + } + } + setDataSourcePriv(srcId, path, keys, values, cookies); + } + + private void setDataSourcePriv(long srcId, String path, String[] keys, String[] values, + List<HttpCookie> cookies) + throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { + final Uri uri = Uri.parse(path); + final String scheme = uri.getScheme(); + if ("file".equals(scheme)) { + path = uri.getPath(); + } else if (scheme != null) { + // handle non-file sources + nativeSetDataSource( + srcId, + Media2HTTPService.createHTTPService(path, cookies), + path, + keys, + values); + return; + } + + final File file = new File(path); + if (file.exists()) { + FileInputStream is = new FileInputStream(file); + FileDescriptor fd = is.getFD(); + setDataSourcePriv(srcId, fd, 0, DataSourceDesc.LONG_MAX); + is.close(); + } else { + throw new IOException("setDataSourcePriv failed."); + } + } + + private native void nativeSetDataSource( + long srcId, Media2HTTPService httpService, String path, String[] keys, String[] values) + throws IOException, IllegalArgumentException, SecurityException, IllegalStateException; + + /** + * Sets the data source (FileDescriptor) to use. The FileDescriptor must be + * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility + * to close the file descriptor. It is safe to do so as soon as this call returns. + * + * @throws IllegalStateException if it is called in an invalid state + * @throws IllegalArgumentException if fd is not a valid FileDescriptor + * @throws IOException if fd can not be read + */ + private void setDataSourcePriv(long srcId, FileDescriptor fd, long offset, long length) + throws IOException { + _setDataSource(srcId, fd, offset, length); + } + + private native void _setDataSource(long srcId, FileDescriptor fd, long offset, long length) + throws IOException; + + /** + * @throws IllegalStateException if it is called in an invalid state + * @throws IllegalArgumentException if dataSource is not a valid Media2DataSource + */ + private void setDataSourcePriv(long srcId, Media2DataSource dataSource) { + _setDataSource(srcId, dataSource); + } + + private native void _setDataSource(long srcId, Media2DataSource dataSource); + + /** + * Prepares the player for playback, synchronously. + * + * After setting the datasource and the display surface, you need to either + * call prepare() or prepareAsync(). For files, it is OK to call prepare(), + * which blocks until MediaPlayer2 is ready for playback. + * + * @throws IOException if source can not be accessed + * @throws IllegalStateException if it is called in an invalid state + * @hide + */ + @Override + public void prepare() throws IOException { + _prepare(); + scanInternalSubtitleTracks(); + + // DrmInfo, if any, has been resolved by now. + synchronized (mDrmLock) { + mDrmInfoResolved = true; + } + } + + private native void _prepare() throws IOException, IllegalStateException; + + /** + * Prepares the player for playback, asynchronously. + * + * After setting the datasource and the display surface, you need to either + * call prepare() or prepareAsync(). For streams, you should call prepareAsync(), + * which returns immediately, rather than blocking until enough data has been + * buffered. + * + * @throws IllegalStateException if it is called in an invalid state + */ + @Override + public native void prepareAsync(); + + /** + * Starts or resumes playback. If playback had previously been paused, + * playback will continue from where it was paused. If playback had + * been stopped, or never started before, playback will start at the + * beginning. + * + * @throws IllegalStateException if it is called in an invalid state + */ + @Override + public void play() { + stayAwake(true); + _start(); + } + + private native void _start() throws IllegalStateException; + + + private int getAudioStreamType() { + if (mStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) { + mStreamType = _getAudioStreamType(); + } + return mStreamType; + } + + private native int _getAudioStreamType() throws IllegalStateException; + + /** + * Stops playback after playback has been started or paused. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + * #hide + */ + @Override + public void stop() { + stayAwake(false); + _stop(); + } + + private native void _stop() throws IllegalStateException; + + /** + * Pauses playback. Call play() to resume. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + */ + @Override + public void pause() { + stayAwake(false); + _pause(); + } + + private native void _pause() throws IllegalStateException; + + //-------------------------------------------------------------------------- + // Explicit Routing + //-------------------- + private AudioDeviceInfo mPreferredDevice = null; + + /** + * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route + * the output from this MediaPlayer2. + * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source. + * If deviceInfo is null, default routing is restored. + * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and + * does not correspond to a valid audio device. + */ + @Override + public boolean setPreferredDevice(AudioDeviceInfo deviceInfo) { + if (deviceInfo != null && !deviceInfo.isSink()) { + return false; + } + int preferredDeviceId = deviceInfo != null ? deviceInfo.getId() : 0; + boolean status = native_setOutputDevice(preferredDeviceId); + if (status == true) { + synchronized (this) { + mPreferredDevice = deviceInfo; + } + } + return status; + } + + /** + * Returns the selected output specified by {@link #setPreferredDevice}. Note that this + * is not guaranteed to correspond to the actual device being used for playback. + */ + @Override + public AudioDeviceInfo getPreferredDevice() { + synchronized (this) { + return mPreferredDevice; + } + } + + /** + * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer2 + * Note: The query is only valid if the MediaPlayer2 is currently playing. + * If the player is not playing, the returned device can be null or correspond to previously + * selected device when the player was last active. + */ + @Override + public AudioDeviceInfo getRoutedDevice() { + int deviceId = native_getRoutedDeviceId(); + if (deviceId == 0) { + return null; + } + AudioDeviceInfo[] devices = + AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS); + for (int i = 0; i < devices.length; i++) { + if (devices[i].getId() == deviceId) { + return devices[i]; + } + } + return null; + } + + /* + * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler. + */ + @GuardedBy("mRoutingChangeListeners") + private void enableNativeRoutingCallbacksLocked(boolean enabled) { + if (mRoutingChangeListeners.size() == 0) { + native_enableDeviceCallback(enabled); + } + } + + /** + * The list of AudioRouting.OnRoutingChangedListener interfaces added (with + * {@link #addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, Handler)} + * by an app to receive (re)routing notifications. + */ + @GuardedBy("mRoutingChangeListeners") + private ArrayMap<AudioRouting.OnRoutingChangedListener, + NativeRoutingEventHandlerDelegate> mRoutingChangeListeners = new ArrayMap<>(); + + /** + * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing + * changes on this MediaPlayer2. + * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive + * notifications of rerouting events. + * @param handler Specifies the {@link Handler} object for the thread on which to execute + * the callback. If <code>null</code>, the handler on the main looper will be used. + */ + @Override + public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener, + Handler handler) { + synchronized (mRoutingChangeListeners) { + if (listener != null && !mRoutingChangeListeners.containsKey(listener)) { + enableNativeRoutingCallbacksLocked(true); + mRoutingChangeListeners.put( + listener, new NativeRoutingEventHandlerDelegate(this, listener, + handler != null ? handler : mEventHandler)); + } + } + } + + /** + * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added + * to receive rerouting notifications. + * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface + * to remove. + */ + @Override + public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener) { + synchronized (mRoutingChangeListeners) { + if (mRoutingChangeListeners.containsKey(listener)) { + mRoutingChangeListeners.remove(listener); + enableNativeRoutingCallbacksLocked(false); + } + } + } + + private native final boolean native_setOutputDevice(int deviceId); + private native final int native_getRoutedDeviceId(); + private native final void native_enableDeviceCallback(boolean enabled); + + /** + * Set the low-level power management behavior for this MediaPlayer2. This + * can be used when the MediaPlayer2 is not playing through a SurfaceHolder + * set with {@link #setDisplay(SurfaceHolder)} and thus can use the + * high-level {@link #setScreenOnWhilePlaying(boolean)} feature. + * + * <p>This function has the MediaPlayer2 access the low-level power manager + * service to control the device's power usage while playing is occurring. + * The parameter is a combination of {@link android.os.PowerManager} wake flags. + * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK} + * permission. + * By default, no attempt is made to keep the device awake during playback. + * + * @param context the Context to use + * @param mode the power/wake mode to set + * @see android.os.PowerManager + * @hide + */ + @Override + public void setWakeMode(Context context, int mode) { + boolean washeld = false; + + /* Disable persistant wakelocks in media player based on property */ + if (SystemProperties.getBoolean("audio.offload.ignore_setawake", false) == true) { + Log.w(TAG, "IGNORING setWakeMode " + mode); + return; + } + + if (mWakeLock != null) { + if (mWakeLock.isHeld()) { + washeld = true; + mWakeLock.release(); + } + mWakeLock = null; + } + + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(mode|PowerManager.ON_AFTER_RELEASE, MediaPlayer2Impl.class.getName()); + mWakeLock.setReferenceCounted(false); + if (washeld) { + mWakeLock.acquire(); + } + } + + /** + * Control whether we should use the attached SurfaceHolder to keep the + * screen on while video playback is occurring. This is the preferred + * method over {@link #setWakeMode} where possible, since it doesn't + * require that the application have permission for low-level wake lock + * access. + * + * @param screenOn Supply true to keep the screen on, false to allow it + * to turn off. + * @hide + */ + @Override + public void setScreenOnWhilePlaying(boolean screenOn) { + if (mScreenOnWhilePlaying != screenOn) { + if (screenOn && mSurfaceHolder == null) { + Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective without a SurfaceHolder"); + } + mScreenOnWhilePlaying = screenOn; + updateSurfaceScreenOn(); + } + } + + private void stayAwake(boolean awake) { + if (mWakeLock != null) { + if (awake && !mWakeLock.isHeld()) { + mWakeLock.acquire(); + } else if (!awake && mWakeLock.isHeld()) { + mWakeLock.release(); + } + } + mStayAwake = awake; + updateSurfaceScreenOn(); + } + + private void updateSurfaceScreenOn() { + if (mSurfaceHolder != null) { + mSurfaceHolder.setKeepScreenOn(mScreenOnWhilePlaying && mStayAwake); + } + } + + /** + * Returns the width of the video. + * + * @return the width of the video, or 0 if there is no video, + * no display surface was set, or the width has not been determined + * yet. The {@code EventCallback} can be registered via + * {@link #registerEventCallback(Executor, EventCallback)} to provide a + * notification {@code EventCallback.onVideoSizeChanged} when the width is available. + */ + @Override + public native int getVideoWidth(); + + /** + * Returns the height of the video. + * + * @return the height of the video, or 0 if there is no video, + * no display surface was set, or the height has not been determined + * yet. The {@code EventCallback} can be registered via + * {@link #registerEventCallback(Executor, EventCallback)} to provide a + * notification {@code EventCallback.onVideoSizeChanged} when the height is available. + */ + @Override + public native int getVideoHeight(); + + /** + * Return Metrics data about the current player. + * + * @return a {@link PersistableBundle} containing the set of attributes and values + * available for the media being handled by this instance of MediaPlayer2 + * The attributes are descibed in {@link MetricsConstants}. + * + * Additional vendor-specific fields may also be present in + * the return value. + */ + @Override + public PersistableBundle getMetrics() { + PersistableBundle bundle = native_getMetrics(); + return bundle; + } + + private native PersistableBundle native_getMetrics(); + + /** + * Checks whether the MediaPlayer2 is playing. + * + * @return true if currently playing, false otherwise + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released. + */ + @Override + public native boolean isPlaying(); + + /** + * Gets the current buffering management params used by the source component. + * Calling it only after {@code setDataSource} has been called. + * Each type of data source might have different set of default params. + * + * @return the current buffering management params used by the source component. + * @throws IllegalStateException if the internal player engine has not been + * initialized, or {@code setDataSource} has not been called. + * @hide + */ + @Override + @NonNull + public native BufferingParams getBufferingParams(); + + /** + * Sets buffering management params. + * The object sets its internal BufferingParams to the input, except that the input is + * invalid or not supported. + * Call it only after {@code setDataSource} has been called. + * The input is a hint to MediaPlayer2. + * + * @param params the buffering management params. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released, or {@code setDataSource} has not been called. + * @throws IllegalArgumentException if params is invalid or not supported. + * @hide + */ + @Override + public native void setBufferingParams(@NonNull BufferingParams params); + + /** + * Sets playback rate and audio mode. + * + * @param rate the ratio between desired playback rate and normal one. + * @param audioMode audio playback mode. Must be one of the supported + * audio modes. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + * @throws IllegalArgumentException if audioMode is not supported. + * + * @hide + */ + @Override + @NonNull + public PlaybackParams easyPlaybackParams(float rate, @PlaybackRateAudioMode int audioMode) { + PlaybackParams params = new PlaybackParams(); + params.allowDefaults(); + switch (audioMode) { + case PLAYBACK_RATE_AUDIO_MODE_DEFAULT: + params.setSpeed(rate).setPitch(1.0f); + break; + case PLAYBACK_RATE_AUDIO_MODE_STRETCH: + params.setSpeed(rate).setPitch(1.0f) + .setAudioFallbackMode(params.AUDIO_FALLBACK_MODE_FAIL); + break; + case PLAYBACK_RATE_AUDIO_MODE_RESAMPLE: + params.setSpeed(rate).setPitch(rate); + break; + default: + final String msg = "Audio playback mode " + audioMode + " is not supported"; + throw new IllegalArgumentException(msg); + } + return params; + } + + /** + * Sets playback rate using {@link PlaybackParams}. The object sets its internal + * PlaybackParams to the input, except that the object remembers previous speed + * when input speed is zero. This allows the object to resume at previous speed + * when play() is called. Calling it before the object is prepared does not change + * the object state. After the object is prepared, calling it with zero speed is + * equivalent to calling pause(). After the object is prepared, calling it with + * non-zero speed is equivalent to calling play(). + * + * @param params the playback params. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized or has been released. + * @throws IllegalArgumentException if params is not supported. + */ + @Override + public native void setPlaybackParams(@NonNull PlaybackParams params); + + /** + * Gets the playback params, containing the current playback rate. + * + * @return the playback params. + * @throws IllegalStateException if the internal player engine has not been + * initialized. + */ + @Override + @NonNull + public native PlaybackParams getPlaybackParams(); + + /** + * Sets A/V sync mode. + * + * @param params the A/V sync params to apply + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + * @throws IllegalArgumentException if params are not supported. + */ + @Override + public native void setSyncParams(@NonNull SyncParams params); + + /** + * Gets the A/V sync mode. + * + * @return the A/V sync params + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + */ + @Override + @NonNull + public native SyncParams getSyncParams(); + + private native final void _seekTo(long msec, int mode); + + /** + * Moves the media to specified time position by considering the given mode. + * <p> + * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user. + * There is at most one active seekTo processed at any time. If there is a to-be-completed + * seekTo, new seekTo requests will be queued in such a way that only the last request + * is kept. When current seekTo is completed, the queued request will be processed if + * that request is different from just-finished seekTo operation, i.e., the requested + * position or mode is different. + * + * @param msec the offset in milliseconds from the start to seek to. + * When seeking to the given time position, there is no guarantee that the data source + * has a frame located at the position. When this happens, a frame nearby will be rendered. + * If msec is negative, time position zero will be used. + * If msec is larger than duration, duration will be used. + * @param mode the mode indicating where exactly to seek to. + * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame + * that has a timestamp earlier than or the same as msec. Use + * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame + * that has a timestamp later than or the same as msec. Use + * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame + * that has a timestamp closest to or the same as msec. Use + * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may + * or may not be a sync frame but is closest to or the same as msec. + * {@link #SEEK_CLOSEST} often has larger performance overhead compared + * to the other options if there is no sync frame located at msec. + * @throws IllegalStateException if the internal player engine has not been + * initialized + * @throws IllegalArgumentException if the mode is invalid. + */ + @Override + public void seekTo(long msec, @SeekMode int mode) { + if (mode < SEEK_PREVIOUS_SYNC || mode > SEEK_CLOSEST) { + final String msg = "Illegal seek mode: " + mode; + throw new IllegalArgumentException(msg); + } + // TODO: pass long to native, instead of truncating here. + if (msec > Integer.MAX_VALUE) { + Log.w(TAG, "seekTo offset " + msec + " is too large, cap to " + Integer.MAX_VALUE); + msec = Integer.MAX_VALUE; + } else if (msec < Integer.MIN_VALUE) { + Log.w(TAG, "seekTo offset " + msec + " is too small, cap to " + Integer.MIN_VALUE); + msec = Integer.MIN_VALUE; + } + _seekTo(msec, mode); + } + + /** + * Get current playback position as a {@link MediaTimestamp}. + * <p> + * The MediaTimestamp represents how the media time correlates to the system time in + * a linear fashion using an anchor and a clock rate. During regular playback, the media + * time moves fairly constantly (though the anchor frame may be rebased to a current + * system time, the linear correlation stays steady). Therefore, this method does not + * need to be called often. + * <p> + * To help users get current playback position, this method always anchors the timestamp + * to the current {@link System#nanoTime system time}, so + * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position. + * + * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp + * is available, e.g. because the media player has not been initialized. + * + * @see MediaTimestamp + */ + @Override + @Nullable + public MediaTimestamp getTimestamp() + { + try { + // TODO: get the timestamp from native side + return new MediaTimestamp( + getCurrentPosition() * 1000L, + System.nanoTime(), + isPlaying() ? getPlaybackParams().getSpeed() : 0.f); + } catch (IllegalStateException e) { + return null; + } + } + + /** + * Gets the current playback position. + * + * @return the current position in milliseconds + */ + @Override + public native int getCurrentPosition(); + + /** + * Gets the duration of the file. + * + * @return the duration in milliseconds, if no duration is available + * (for example, if streaming live content), -1 is returned. + */ + @Override + public native int getDuration(); + + /** + * Gets the media metadata. + * + * @param update_only controls whether the full set of available + * metadata is returned or just the set that changed since the + * last call. See {@see #METADATA_UPDATE_ONLY} and {@see + * #METADATA_ALL}. + * + * @param apply_filter if true only metadata that matches the + * filter is returned. See {@see #APPLY_METADATA_FILTER} and {@see + * #BYPASS_METADATA_FILTER}. + * + * @return The metadata, possibly empty. null if an error occured. + // FIXME: unhide. + * {@hide} + */ + @Override + public Metadata getMetadata(final boolean update_only, + final boolean apply_filter) { + Parcel reply = Parcel.obtain(); + Metadata data = new Metadata(); + + if (!native_getMetadata(update_only, apply_filter, reply)) { + reply.recycle(); + return null; + } + + // Metadata takes over the parcel, don't recycle it unless + // there is an error. + if (!data.parse(reply)) { + reply.recycle(); + return null; + } + return data; + } + + /** + * Set a filter for the metadata update notification and update + * retrieval. The caller provides 2 set of metadata keys, allowed + * and blocked. The blocked set always takes precedence over the + * allowed one. + * Metadata.MATCH_ALL and Metadata.MATCH_NONE are 2 sets available as + * shorthands to allow/block all or no metadata. + * + * By default, there is no filter set. + * + * @param allow Is the set of metadata the client is interested + * in receiving new notifications for. + * @param block Is the set of metadata the client is not interested + * in receiving new notifications for. + * @return The call status code. + * + // FIXME: unhide. + * {@hide} + */ + @Override + public int setMetadataFilter(Set<Integer> allow, Set<Integer> block) { + // Do our serialization manually instead of calling + // Parcel.writeArray since the sets are made of the same type + // we avoid paying the price of calling writeValue (used by + // writeArray) which burns an extra int per element to encode + // the type. + Parcel request = newRequest(); + + // The parcel starts already with an interface token. There + // are 2 filters. Each one starts with a 4bytes number to + // store the len followed by a number of int (4 bytes as well) + // representing the metadata type. + int capacity = request.dataSize() + 4 * (1 + allow.size() + 1 + block.size()); + + if (request.dataCapacity() < capacity) { + request.setDataCapacity(capacity); + } + + request.writeInt(allow.size()); + for(Integer t: allow) { + request.writeInt(t); + } + request.writeInt(block.size()); + for(Integer t: block) { + request.writeInt(t); + } + return native_setMetadataFilter(request); + } + + /** + * Set the MediaPlayer2 to start when this MediaPlayer2 finishes playback + * (i.e. reaches the end of the stream). + * The media framework will attempt to transition from this player to + * the next as seamlessly as possible. The next player can be set at + * any time before completion, but shall be after setDataSource has been + * called successfully. The next player must be prepared by the + * app, and the application should not call play() on it. + * The next MediaPlayer2 must be different from 'this'. An exception + * will be thrown if next == this. + * The application may call setNextMediaPlayer(null) to indicate no + * next player should be started at the end of playback. + * If the current player is looping, it will keep looping and the next + * player will not be started. + * + * @param next the player to start after this one completes playback. + * + * @hide + */ + @Override + public native void setNextMediaPlayer(MediaPlayer2 next); + + /** + * Resets the MediaPlayer2 to its uninitialized state. After calling + * this method, you will have to initialize it again by setting the + * data source and calling prepare(). + */ + @Override + public void reset() { + mSelectedSubtitleTrackIndex = -1; + synchronized(mOpenSubtitleSources) { + for (final InputStream is: mOpenSubtitleSources) { + try { + is.close(); + } catch (IOException e) { + } + } + mOpenSubtitleSources.clear(); + } + if (mSubtitleController != null) { + mSubtitleController.reset(); + } + if (mTimeProvider != null) { + mTimeProvider.close(); + mTimeProvider = null; + } + + synchronized (mEventCbLock) { + mEventCallbackRecords.clear(); + } + synchronized (mDrmEventCbLock) { + mDrmEventCallbackRecords.clear(); + } + + stayAwake(false); + _reset(); + // make sure none of the listeners get called anymore + if (mEventHandler != null) { + mEventHandler.removeCallbacksAndMessages(null); + } + + synchronized (mIndexTrackPairs) { + mIndexTrackPairs.clear(); + mInbandTrackIndices.clear(); + }; + + resetDrmState(); + } + + private native void _reset(); + + /** + * Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be + * notified when the presentation time reaches (becomes greater than or equal to) + * the value specified. + * + * @param mediaTimeUs presentation time to get timed event callback at + * @hide + */ + @Override + public void notifyAt(long mediaTimeUs) { + _notifyAt(mediaTimeUs); + } + + private native void _notifyAt(long mediaTimeUs); + + // Keep KEY_PARAMETER_* in sync with include/media/mediaplayer2.h + private final static int KEY_PARAMETER_AUDIO_ATTRIBUTES = 1400; + /** + * Sets the parameter indicated by key. + * @param key key indicates the parameter to be set. + * @param value value of the parameter to be set. + * @return true if the parameter is set successfully, false otherwise + * {@hide} + */ + private native boolean setParameter(int key, Parcel value); + + /** + * Sets the audio attributes for this MediaPlayer2. + * See {@link AudioAttributes} for how to build and configure an instance of this class. + * You must call this method before {@link #prepare()} or {@link #prepareAsync()} in order + * for the audio attributes to become effective thereafter. + * @param attributes a non-null set of audio attributes + * @throws IllegalArgumentException if the attributes are null or invalid. + */ + @Override + public void setAudioAttributes(AudioAttributes attributes) { + if (attributes == null) { + final String msg = "Cannot set AudioAttributes to null"; + throw new IllegalArgumentException(msg); + } + mUsage = attributes.getUsage(); + mBypassInterruptionPolicy = (attributes.getAllFlags() + & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0; + Parcel pattributes = Parcel.obtain(); + attributes.writeToParcel(pattributes, AudioAttributes.FLATTEN_TAGS); + setParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES, pattributes); + pattributes.recycle(); + } + + /** + * Sets the player to be looping or non-looping. + * + * @param looping whether to loop or not + * @hide + */ + @Override + public native void setLooping(boolean looping); + + /** + * Checks whether the MediaPlayer2 is looping or non-looping. + * + * @return true if the MediaPlayer2 is currently looping, false otherwise + * @hide + */ + @Override + public native boolean isLooping(); + + /** + * Sets the volume on this player. + * This API is recommended for balancing the output of audio streams + * within an application. Unless you are writing an application to + * control user settings, this API should be used in preference to + * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of + * a particular type. Note that the passed volume values are raw scalars in range 0.0 to 1.0. + * UI controls should be scaled logarithmically. + * + * @param leftVolume left volume scalar + * @param rightVolume right volume scalar + */ + /* + * FIXME: Merge this into javadoc comment above when setVolume(float) is not @hide. + * The single parameter form below is preferred if the channel volumes don't need + * to be set independently. + */ + @Override + public void setVolume(float leftVolume, float rightVolume) { + _setVolume(leftVolume, rightVolume); + } + + private native void _setVolume(float leftVolume, float rightVolume); + + /** + * Similar, excepts sets volume of all channels to same value. + * @hide + */ + @Override + public void setVolume(float volume) { + setVolume(volume, volume); + } + + /** + * Sets the audio session ID. + * + * @param sessionId the audio session ID. + * The audio session ID is a system wide unique identifier for the audio stream played by + * this MediaPlayer2 instance. + * The primary use of the audio session ID is to associate audio effects to a particular + * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect, + * this effect will be applied only to the audio content of media players within the same + * audio session and not to the output mix. + * When created, a MediaPlayer2 instance automatically generates its own audio session ID. + * However, it is possible to force this player to be part of an already existing audio session + * by calling this method. + * This method must be called before one of the overloaded <code> setDataSource </code> methods. + * @throws IllegalStateException if it is called in an invalid state + * @throws IllegalArgumentException if the sessionId is invalid. + */ + @Override + public native void setAudioSessionId(int sessionId); + + /** + * Returns the audio session ID. + * + * @return the audio session ID. {@see #setAudioSessionId(int)} + * Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was contructed. + */ + @Override + public native int getAudioSessionId(); + + /** + * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation + * effect which can be applied on any sound source that directs a certain amount of its + * energy to this effect. This amount is defined by setAuxEffectSendLevel(). + * See {@link #setAuxEffectSendLevel(float)}. + * <p>After creating an auxiliary effect (e.g. + * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with + * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method + * to attach the player to the effect. + * <p>To detach the effect from the player, call this method with a null effect id. + * <p>This method must be called after one of the overloaded <code> setDataSource </code> + * methods. + * @param effectId system wide unique id of the effect to attach + */ + @Override + public native void attachAuxEffect(int effectId); + + + /** + * Sets the send level of the player to the attached auxiliary effect. + * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0. + * <p>By default the send level is 0, so even if an effect is attached to the player + * this method must be called for the effect to be applied. + * <p>Note that the passed level value is a raw scalar. UI controls should be scaled + * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB, + * so an appropriate conversion from linear UI input x to level is: + * x == 0 -> level = 0 + * 0 < x <= R -> level = 10^(72*(x-R)/20/R) + * @param level send level scalar + */ + @Override + public void setAuxEffectSendLevel(float level) { + _setAuxEffectSendLevel(level); + } + + private native void _setAuxEffectSendLevel(float level); + + /* + * @param request Parcel destinated to the media player. + * @param reply[out] Parcel that will contain the reply. + * @return The status code. + */ + private native final int native_invoke(Parcel request, Parcel reply); + + + /* + * @param update_only If true fetch only the set of metadata that have + * changed since the last invocation of getMetadata. + * The set is built using the unfiltered + * notifications the native player sent to the + * MediaPlayer2Manager during that period of + * time. If false, all the metadatas are considered. + * @param apply_filter If true, once the metadata set has been built based on + * the value update_only, the current filter is applied. + * @param reply[out] On return contains the serialized + * metadata. Valid only if the call was successful. + * @return The status code. + */ + private native final boolean native_getMetadata(boolean update_only, + boolean apply_filter, + Parcel reply); + + /* + * @param request Parcel with the 2 serialized lists of allowed + * metadata types followed by the one to be + * dropped. Each list starts with an integer + * indicating the number of metadata type elements. + * @return The status code. + */ + private native final int native_setMetadataFilter(Parcel request); + + private static native final void native_init(); + private native final void native_setup(Object mediaplayer2_this); + private native final void native_finalize(); + + private static native final void native_stream_event_onTearDown( + long nativeCallbackPtr, long userDataPtr); + private static native final void native_stream_event_onStreamPresentationEnd( + long nativeCallbackPtr, long userDataPtr); + private static native final void native_stream_event_onStreamDataRequest( + long jAudioTrackPtr, long nativeCallbackPtr, long userDataPtr); + + /** + * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata. + * + * @see android.media.MediaPlayer2#getTrackInfo + */ + public static final class TrackInfoImpl extends TrackInfo { + /** + * Gets the track type. + * @return TrackType which indicates if the track is video, audio, timed text. + */ + @Override + public int getTrackType() { + return mTrackType; + } + + /** + * Gets the language code of the track. + * @return a language code in either way of ISO-639-1 or ISO-639-2. + * When the language is unknown or could not be determined, + * ISO-639-2 language code, "und", is returned. + */ + @Override + public String getLanguage() { + String language = mFormat.getString(MediaFormat.KEY_LANGUAGE); + return language == null ? "und" : language; + } + + /** + * Gets the {@link MediaFormat} of the track. If the format is + * unknown or could not be determined, null is returned. + */ + @Override + public MediaFormat getFormat() { + if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT + || mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) { + return mFormat; + } + return null; + } + + final int mTrackType; + final MediaFormat mFormat; + + TrackInfoImpl(Parcel in) { + mTrackType = in.readInt(); + // TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat + // even for audio/video tracks, meaning we only set the mime and language. + String mime = in.readString(); + String language = in.readString(); + mFormat = MediaFormat.createSubtitleFormat(mime, language); + + if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) { + 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()); + } + } + + /** @hide */ + TrackInfoImpl(int type, MediaFormat format) { + mTrackType = type; + mFormat = format; + } + + /** + * Flatten this object in to a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}. + */ + /* package private */ void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mTrackType); + 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 TrackInfoImpl from a Parcel. + */ + /* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR + = new Parcelable.Creator<TrackInfoImpl>() { + @Override + public TrackInfoImpl createFromParcel(Parcel in) { + return new TrackInfoImpl(in); + } + + @Override + public TrackInfoImpl[] newArray(int size) { + return new TrackInfoImpl[size]; + } + }; + + }; + + // We would like domain specific classes with more informative names than the `first` and `second` + // in generic Pair, but we would also like to avoid creating new/trivial classes. As a compromise + // we document the meanings of `first` and `second` here: + // + // Pair.first - inband track index; non-null iff representing an inband track. + // Pair.second - a SubtitleTrack registered with mSubtitleController; non-null iff representing + // an inband subtitle track or any out-of-band track (subtitle or timedtext). + private Vector<Pair<Integer, SubtitleTrack>> mIndexTrackPairs = new Vector<>(); + private BitSet mInbandTrackIndices = new BitSet(); + + /** + * Returns a List of track information. + * + * @return List of track info. The total number of tracks is the array length. + * Must be called again if an external timed text source has been added after + * addTimedTextSource method is called. + * @throws IllegalStateException if it is called in an invalid state. + */ + @Override + public List<TrackInfo> getTrackInfo() { + TrackInfoImpl trackInfo[] = getInbandTrackInfoImpl(); + // add out-of-band tracks + synchronized (mIndexTrackPairs) { + TrackInfoImpl allTrackInfo[] = new TrackInfoImpl[mIndexTrackPairs.size()]; + for (int i = 0; i < allTrackInfo.length; i++) { + Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i); + if (p.first != null) { + // inband track + allTrackInfo[i] = trackInfo[p.first]; + } else { + SubtitleTrack track = p.second; + allTrackInfo[i] = new TrackInfoImpl(track.getTrackType(), track.getFormat()); + } + } + return Arrays.asList(allTrackInfo); + } + } + + private TrackInfoImpl[] getInbandTrackInfoImpl() throws IllegalStateException { + Parcel request = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + try { + request.writeInt(INVOKE_ID_GET_TRACK_INFO); + invoke(request, reply); + TrackInfoImpl trackInfo[] = reply.createTypedArray(TrackInfoImpl.CREATOR); + return trackInfo; + } finally { + request.recycle(); + reply.recycle(); + } + } + + /* + * A helper function to check if the mime type is supported by media framework. + */ + private static boolean availableMimeTypeForExternalSource(String mimeType) { + if (MEDIA_MIMETYPE_TEXT_SUBRIP.equals(mimeType)) { + return true; + } + return false; + } + + private SubtitleController mSubtitleController; + + /** @hide */ + @Override + public void setSubtitleAnchor( + SubtitleController controller, + SubtitleController.Anchor anchor) { + // TODO: create SubtitleController in MediaPlayer2 + mSubtitleController = controller; + mSubtitleController.setAnchor(anchor); + } + + /** + * The private version of setSubtitleAnchor is used internally to set mSubtitleController if + * necessary when clients don't provide their own SubtitleControllers using the public version + * {@link #setSubtitleAnchor(SubtitleController, Anchor)} (e.g. {@link VideoView} provides one). + */ + private synchronized void setSubtitleAnchor() { + if ((mSubtitleController == null) && (ActivityThread.currentApplication() != null)) { + final HandlerThread thread = new HandlerThread("SetSubtitleAnchorThread"); + thread.start(); + Handler handler = new Handler(thread.getLooper()); + handler.post(new Runnable() { + @Override + public void run() { + Context context = ActivityThread.currentApplication(); + mSubtitleController = new SubtitleController(context, mTimeProvider, MediaPlayer2Impl.this); + mSubtitleController.setAnchor(new Anchor() { + @Override + public void setSubtitleWidget(RenderingWidget subtitleWidget) { + } + + @Override + public Looper getSubtitleLooper() { + return Looper.getMainLooper(); + } + }); + thread.getLooper().quitSafely(); + } + }); + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.w(TAG, "failed to join SetSubtitleAnchorThread"); + } + } + } + + private int mSelectedSubtitleTrackIndex = -1; + private Vector<InputStream> mOpenSubtitleSources; + + private OnSubtitleDataListener mSubtitleDataListener = new OnSubtitleDataListener() { + @Override + public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) { + int index = data.getTrackIndex(); + synchronized (mIndexTrackPairs) { + for (Pair<Integer, SubtitleTrack> p : mIndexTrackPairs) { + if (p.first != null && p.first == index && p.second != null) { + // inband subtitle track that owns data + SubtitleTrack track = p.second; + track.onData(data); + } + } + } + } + }; + + /** @hide */ + @Override + public void onSubtitleTrackSelected(SubtitleTrack track) { + if (mSelectedSubtitleTrackIndex >= 0) { + try { + selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, false); + } catch (IllegalStateException e) { + } + mSelectedSubtitleTrackIndex = -1; + } + setOnSubtitleDataListener(null); + if (track == null) { + return; + } + + synchronized (mIndexTrackPairs) { + for (Pair<Integer, SubtitleTrack> p : mIndexTrackPairs) { + if (p.first != null && p.second == track) { + // inband subtitle track that is selected + mSelectedSubtitleTrackIndex = p.first; + break; + } + } + } + + if (mSelectedSubtitleTrackIndex >= 0) { + try { + selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true); + } catch (IllegalStateException e) { + } + setOnSubtitleDataListener(mSubtitleDataListener); + } + // no need to select out-of-band tracks + } + + /** @hide */ + @Override + public void addSubtitleSource(InputStream is, MediaFormat format) + throws IllegalStateException + { + final InputStream fIs = is; + final MediaFormat fFormat = format; + + if (is != null) { + // Ensure all input streams are closed. It is also a handy + // way to implement timeouts in the future. + synchronized(mOpenSubtitleSources) { + mOpenSubtitleSources.add(is); + } + } else { + Log.w(TAG, "addSubtitleSource called with null InputStream"); + } + + getMediaTimeProvider(); + + // process each subtitle in its own thread + final HandlerThread thread = new HandlerThread("SubtitleReadThread", + Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE); + thread.start(); + Handler handler = new Handler(thread.getLooper()); + handler.post(new Runnable() { + private int addTrack() { + if (fIs == null || mSubtitleController == null) { + return MEDIA_INFO_UNSUPPORTED_SUBTITLE; + } + + SubtitleTrack track = mSubtitleController.addTrack(fFormat); + if (track == null) { + return MEDIA_INFO_UNSUPPORTED_SUBTITLE; + } + + // TODO: do the conversion in the subtitle track + Scanner scanner = new Scanner(fIs, "UTF-8"); + String contents = scanner.useDelimiter("\\A").next(); + synchronized(mOpenSubtitleSources) { + mOpenSubtitleSources.remove(fIs); + } + scanner.close(); + synchronized (mIndexTrackPairs) { + mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(null, track)); + } + Handler h = mTimeProvider.mEventHandler; + int what = TimeProvider.NOTIFY; + int arg1 = TimeProvider.NOTIFY_TRACK_DATA; + Pair<SubtitleTrack, byte[]> trackData = Pair.create(track, contents.getBytes()); + Message m = h.obtainMessage(what, arg1, 0, trackData); + h.sendMessage(m); + return MEDIA_INFO_EXTERNAL_METADATA_UPDATE; + } + + public void run() { + int res = addTrack(); + if (mEventHandler != null) { + Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null); + mEventHandler.sendMessage(m); + } + thread.getLooper().quitSafely(); + } + }); + } + + private void scanInternalSubtitleTracks() { + setSubtitleAnchor(); + + populateInbandTracks(); + + if (mSubtitleController != null) { + mSubtitleController.selectDefaultTrack(); + } + } + + private void populateInbandTracks() { + TrackInfoImpl[] tracks = getInbandTrackInfoImpl(); + synchronized (mIndexTrackPairs) { + for (int i = 0; i < tracks.length; i++) { + if (mInbandTrackIndices.get(i)) { + continue; + } else { + mInbandTrackIndices.set(i); + } + + // newly appeared inband track + if (tracks[i].getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) { + SubtitleTrack track = mSubtitleController.addTrack( + tracks[i].getFormat()); + mIndexTrackPairs.add(Pair.create(i, track)); + } else { + mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(i, null)); + } + } + } + } + + /* TODO: Limit the total number of external timed text source to a reasonable number. + */ + /** + * Adds an external timed text source file. + * + * Currently supported format is SubRip with the file extension .srt, case insensitive. + * Note that a single external timed text source may contain multiple tracks in it. + * One can find the total number of available tracks using {@link #getTrackInfo()} to see what + * additional tracks become available after this method call. + * + * @param path The file path of external timed text source file. + * @param mimeType The mime type of the file. Must be one of the mime types listed above. + * @throws IOException if the file cannot be accessed or is corrupted. + * @throws IllegalArgumentException if the mimeType is not supported. + * @throws IllegalStateException if called in an invalid state. + * @hide + */ + @Override + public void addTimedTextSource(String path, String mimeType) + throws IOException { + if (!availableMimeTypeForExternalSource(mimeType)) { + final String msg = "Illegal mimeType for timed text source: " + mimeType; + throw new IllegalArgumentException(msg); + } + + File file = new File(path); + if (file.exists()) { + FileInputStream is = new FileInputStream(file); + FileDescriptor fd = is.getFD(); + addTimedTextSource(fd, mimeType); + is.close(); + } else { + // We do not support the case where the path is not a file. + throw new IOException(path); + } + } + + + /** + * Adds an external timed text source file (Uri). + * + * Currently supported format is SubRip with the file extension .srt, case insensitive. + * Note that a single external timed text source may contain multiple tracks in it. + * One can find the total number of available tracks using {@link #getTrackInfo()} to see what + * additional tracks become available after this method call. + * + * @param context the Context to use when resolving the Uri + * @param uri the Content URI of the data you want to play + * @param mimeType The mime type of the file. Must be one of the mime types listed above. + * @throws IOException if the file cannot be accessed or is corrupted. + * @throws IllegalArgumentException if the mimeType is not supported. + * @throws IllegalStateException if called in an invalid state. + * @hide + */ + @Override + public void addTimedTextSource(Context context, Uri uri, String mimeType) + throws IOException { + String scheme = uri.getScheme(); + if(scheme == null || scheme.equals("file")) { + addTimedTextSource(uri.getPath(), mimeType); + return; + } + + AssetFileDescriptor fd = null; + try { + ContentResolver resolver = context.getContentResolver(); + fd = resolver.openAssetFileDescriptor(uri, "r"); + if (fd == null) { + return; + } + addTimedTextSource(fd.getFileDescriptor(), mimeType); + return; + } catch (SecurityException ex) { + } catch (IOException ex) { + } finally { + if (fd != null) { + fd.close(); + } + } + } + + /** + * Adds an external timed text source file (FileDescriptor). + * + * It is the caller's responsibility to close the file descriptor. + * It is safe to do so as soon as this call returns. + * + * Currently supported format is SubRip. Note that a single external timed text source may + * contain multiple tracks in it. One can find the total number of available tracks + * using {@link #getTrackInfo()} to see what additional tracks become available + * after this method call. + * + * @param fd the FileDescriptor for the file you want to play + * @param mimeType The mime type of the file. Must be one of the mime types listed above. + * @throws IllegalArgumentException if the mimeType is not supported. + * @throws IllegalStateException if called in an invalid state. + * @hide + */ + @Override + public void addTimedTextSource(FileDescriptor fd, String mimeType) { + // intentionally less than LONG_MAX + addTimedTextSource(fd, 0, 0x7ffffffffffffffL, mimeType); + } + + /** + * Adds an external timed text file (FileDescriptor). + * + * It is the caller's responsibility to close the file descriptor. + * It is safe to do so as soon as this call returns. + * + * Currently supported format is SubRip. Note that a single external timed text source may + * contain multiple tracks in it. One can find the total number of available tracks + * using {@link #getTrackInfo()} to see what additional tracks become available + * after this method call. + * + * @param fd the FileDescriptor for the file you want to play + * @param offset the offset into the file where the data to be played starts, in bytes + * @param length the length in bytes of the data to be played + * @param mime The mime type of the file. Must be one of the mime types listed above. + * @throws IllegalArgumentException if the mimeType is not supported. + * @throws IllegalStateException if called in an invalid state. + * @hide + */ + @Override + public void addTimedTextSource(FileDescriptor fd, long offset, long length, String mime) { + if (!availableMimeTypeForExternalSource(mime)) { + throw new IllegalArgumentException("Illegal mimeType for timed text source: " + mime); + } + + final FileDescriptor dupedFd; + try { + dupedFd = Os.dup(fd); + } catch (ErrnoException ex) { + Log.e(TAG, ex.getMessage(), ex); + throw new RuntimeException(ex); + } + + final MediaFormat fFormat = new MediaFormat(); + fFormat.setString(MediaFormat.KEY_MIME, mime); + fFormat.setInteger(MediaFormat.KEY_IS_TIMED_TEXT, 1); + + // A MediaPlayer2 created by a VideoView should already have its mSubtitleController set. + if (mSubtitleController == null) { + setSubtitleAnchor(); + } + + if (!mSubtitleController.hasRendererFor(fFormat)) { + // test and add not atomic + Context context = ActivityThread.currentApplication(); + mSubtitleController.registerRenderer(new SRTRenderer(context, mEventHandler)); + } + final SubtitleTrack track = mSubtitleController.addTrack(fFormat); + synchronized (mIndexTrackPairs) { + mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(null, track)); + } + + getMediaTimeProvider(); + + final long offset2 = offset; + final long length2 = length; + final HandlerThread thread = new HandlerThread( + "TimedTextReadThread", + Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE); + thread.start(); + Handler handler = new Handler(thread.getLooper()); + handler.post(new Runnable() { + private int addTrack() { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + Os.lseek(dupedFd, offset2, OsConstants.SEEK_SET); + byte[] buffer = new byte[4096]; + for (long total = 0; total < length2;) { + int bytesToRead = (int) Math.min(buffer.length, length2 - total); + int bytes = IoBridge.read(dupedFd, buffer, 0, bytesToRead); + if (bytes < 0) { + break; + } else { + bos.write(buffer, 0, bytes); + total += bytes; + } + } + Handler h = mTimeProvider.mEventHandler; + int what = TimeProvider.NOTIFY; + int arg1 = TimeProvider.NOTIFY_TRACK_DATA; + Pair<SubtitleTrack, byte[]> trackData = Pair.create(track, bos.toByteArray()); + Message m = h.obtainMessage(what, arg1, 0, trackData); + h.sendMessage(m); + return MEDIA_INFO_EXTERNAL_METADATA_UPDATE; + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + return MEDIA_INFO_TIMED_TEXT_ERROR; + } finally { + try { + Os.close(dupedFd); + } catch (ErrnoException e) { + Log.e(TAG, e.getMessage(), e); + } + } + } + + public void run() { + int res = addTrack(); + if (mEventHandler != null) { + Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null); + mEventHandler.sendMessage(m); + } + thread.getLooper().quitSafely(); + } + }); + } + + /** + * Returns the index of the audio, video, or subtitle track currently selected for playback, + * The return value is an index into the array returned by {@link #getTrackInfo()}, and can + * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}. + * + * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO}, + * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or + * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE} + * @return index of the audio, video, or subtitle track currently selected for playback; + * a negative integer is returned when there is no selected track for {@code trackType} or + * when {@code trackType} is not one of audio, video, or subtitle. + * @throws IllegalStateException if called after {@link #close()} + * + * @see #getTrackInfo() + * @see #selectTrack(int) + * @see #deselectTrack(int) + */ + @Override + public int getSelectedTrack(int trackType) { + if (mSubtitleController != null + && (trackType == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE + || trackType == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT)) { + SubtitleTrack subtitleTrack = mSubtitleController.getSelectedTrack(); + if (subtitleTrack != null) { + synchronized (mIndexTrackPairs) { + for (int i = 0; i < mIndexTrackPairs.size(); i++) { + Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i); + if (p.second == subtitleTrack && subtitleTrack.getTrackType() == trackType) { + return i; + } + } + } + } + } + + Parcel request = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + try { + request.writeInt(INVOKE_ID_GET_SELECTED_TRACK); + request.writeInt(trackType); + invoke(request, reply); + int inbandTrackIndex = reply.readInt(); + synchronized (mIndexTrackPairs) { + for (int i = 0; i < mIndexTrackPairs.size(); i++) { + Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i); + if (p.first != null && p.first == inbandTrackIndex) { + return i; + } + } + } + return -1; + } finally { + request.recycle(); + reply.recycle(); + } + } + + /** + * Selects a track. + * <p> + * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception. + * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately. + * If a MediaPlayer2 is not in Started state, it just marks the track to be played. + * </p> + * <p> + * In any valid state, if it is called multiple times on the same type of track (ie. Video, + * Audio, Timed Text), the most recent one will be chosen. + * </p> + * <p> + * The first audio and video tracks are selected by default if available, even though + * this method is not called. However, no timed text track will be selected until + * this function is called. + * </p> + * <p> + * Currently, only timed text tracks or audio tracks can be selected via this method. + * In addition, the support for selecting an audio track at runtime is pretty limited + * in that an audio track can only be selected in the <em>Prepared</em> state. + * </p> + * @param index the index of the track to be selected. The valid range of the index + * is 0..total number of track - 1. The total number of tracks as well as the type of + * each individual track can be found by calling {@link #getTrackInfo()} method. + * @throws IllegalStateException if called in an invalid state. + * + * @see android.media.MediaPlayer2#getTrackInfo + */ + @Override + public void selectTrack(int index) { + selectOrDeselectTrack(index, true /* select */); + } + + /** + * Deselect a track. + * <p> + * Currently, the track must be a timed text track and no audio or video tracks can be + * deselected. If the timed text track identified by index has not been + * selected before, it throws an exception. + * </p> + * @param index the index of the track to be deselected. The valid range of the index + * is 0..total number of tracks - 1. The total number of tracks as well as the type of + * each individual track can be found by calling {@link #getTrackInfo()} method. + * @throws IllegalStateException if called in an invalid state. + * + * @see android.media.MediaPlayer2#getTrackInfo + */ + @Override + public void deselectTrack(int index) { + selectOrDeselectTrack(index, false /* select */); + } + + private void selectOrDeselectTrack(int index, boolean select) + throws IllegalStateException { + // handle subtitle track through subtitle controller + populateInbandTracks(); + + Pair<Integer,SubtitleTrack> p = null; + try { + p = mIndexTrackPairs.get(index); + } catch (ArrayIndexOutOfBoundsException e) { + // ignore bad index + return; + } + + SubtitleTrack track = p.second; + if (track == null) { + // inband (de)select + selectOrDeselectInbandTrack(p.first, select); + return; + } + + if (mSubtitleController == null) { + return; + } + + if (!select) { + // out-of-band deselect + if (mSubtitleController.getSelectedTrack() == track) { + mSubtitleController.selectTrack(null); + } else { + Log.w(TAG, "trying to deselect track that was not selected"); + } + return; + } + + // out-of-band select + if (track.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) { + int ttIndex = getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT); + synchronized (mIndexTrackPairs) { + if (ttIndex >= 0 && ttIndex < mIndexTrackPairs.size()) { + Pair<Integer,SubtitleTrack> p2 = mIndexTrackPairs.get(ttIndex); + if (p2.first != null && p2.second == null) { + // deselect inband counterpart + selectOrDeselectInbandTrack(p2.first, false); + } + } + } + } + mSubtitleController.selectTrack(track); + } + + private void selectOrDeselectInbandTrack(int index, boolean select) + throws IllegalStateException { + Parcel request = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + try { + request.writeInt(select? INVOKE_ID_SELECT_TRACK: INVOKE_ID_DESELECT_TRACK); + request.writeInt(index); + invoke(request, reply); + } finally { + request.recycle(); + reply.recycle(); + } + } + + /** + * Releases the resources held by this {@code MediaPlayer2} object. + * + * It is considered good practice to call this method when you're + * done using the MediaPlayer2. In particular, whenever an Activity + * of an application is paused (its onPause() method is called), + * or stopped (its onStop() method is called), this method should be + * invoked to release the MediaPlayer2 object, unless the application + * has a special need to keep the object around. In addition to + * unnecessary resources (such as memory and instances of codecs) + * being held, failure to call this method immediately if a + * MediaPlayer2 object is no longer needed may also lead to + * continuous battery consumption for mobile devices, and playback + * failure for other applications if no multiple instances of the + * same codec are supported on a device. Even if multiple instances + * of the same codec are supported, some performance degradation + * may be expected when unnecessary multiple instances are used + * at the same time. + * + * {@code close()} may be safely called after a prior {@code close()}. + * This class implements the Java {@code AutoCloseable} interface and + * may be used with try-with-resources. + */ + @Override + public void close() { + synchronized (mGuard) { + release(); + } + } + + // Have to declare protected for finalize() since it is protected + // in the base class Object. + @Override + protected void finalize() throws Throwable { + if (mGuard != null) { + mGuard.warnIfOpen(); + } + + close(); + native_finalize(); + } + + private void release() { + stayAwake(false); + updateSurfaceScreenOn(); + synchronized (mEventCbLock) { + mEventCallbackRecords.clear(); + } + if (mTimeProvider != null) { + mTimeProvider.close(); + mTimeProvider = null; + } + mOnSubtitleDataListener = null; + + // Modular DRM clean up + mOnDrmConfigHelper = null; + synchronized (mDrmEventCbLock) { + mDrmEventCallbackRecords.clear(); + } + resetDrmState(); + + _release(); + } + + private native void _release(); + + /* Do not change these values without updating their counterparts + * in include/media/mediaplayer2.h! + */ + private static final int MEDIA_NOP = 0; // interface test message + private static final int MEDIA_PREPARED = 1; + private static final int MEDIA_PLAYBACK_COMPLETE = 2; + private static final int MEDIA_BUFFERING_UPDATE = 3; + private static final int MEDIA_SEEK_COMPLETE = 4; + private static final int MEDIA_SET_VIDEO_SIZE = 5; + private static final int MEDIA_STARTED = 6; + private static final int MEDIA_PAUSED = 7; + private static final int MEDIA_STOPPED = 8; + private static final int MEDIA_SKIPPED = 9; + private static final int MEDIA_NOTIFY_TIME = 98; + private static final int MEDIA_TIMED_TEXT = 99; + private static final int MEDIA_ERROR = 100; + private static final int MEDIA_INFO = 200; + private static final int MEDIA_SUBTITLE_DATA = 201; + private static final int MEDIA_META_DATA = 202; + private static final int MEDIA_DRM_INFO = 210; + private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000; + + private TimeProvider mTimeProvider; + + /** @hide */ + @Override + public MediaTimeProvider getMediaTimeProvider() { + if (mTimeProvider == null) { + mTimeProvider = new TimeProvider(this); + } + return mTimeProvider; + } + + private class EventHandler extends Handler { + private MediaPlayer2Impl mMediaPlayer; + + public EventHandler(MediaPlayer2Impl mp, Looper looper) { + super(looper); + mMediaPlayer = mp; + } + + @Override + public void handleMessage(Message msg) { + handleMessage(msg, 0); + } + + public void handleMessage(Message msg, long srcId) { + if (mMediaPlayer.mNativeContext == 0) { + Log.w(TAG, "mediaplayer2 went away with unhandled events"); + return; + } + final int what = msg.arg1; + final int extra = msg.arg2; + switch(msg.what) { + case MEDIA_PREPARED: + try { + scanInternalSubtitleTracks(); + } catch (RuntimeException e) { + // send error message instead of crashing; + // send error message instead of inlining a call to onError + // to avoid code duplication. + Message msg2 = obtainMessage( + MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null); + sendMessage(msg2); + } + + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onInfo( + mMediaPlayer, srcId, MEDIA_INFO_PREPARED, 0)); + } + } + return; + + case MEDIA_DRM_INFO: + if (msg.obj == null) { + Log.w(TAG, "MEDIA_DRM_INFO msg.obj=NULL"); + } else if (msg.obj instanceof Parcel) { + // The parcel was parsed already in postEventFromNative + final DrmInfoImpl drmInfo; + + synchronized (mDrmLock) { + if (mDrmInfoImpl != null) { + drmInfo = mDrmInfoImpl.makeCopy(); + } else { + drmInfo = null; + } + } + + // notifying the client outside the lock + if (drmInfo != null) { + synchronized (mEventCbLock) { + for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) { + cb.first.execute(() -> cb.second.onDrmInfo( + mMediaPlayer, drmInfo)); + } + } + } + } else { + Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + msg.obj); + } + return; + + case MEDIA_PLAYBACK_COMPLETE: + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onInfo( + mMediaPlayer, srcId, MEDIA_INFO_PLAYBACK_COMPLETE, 0)); + } + } + stayAwake(false); + return; + + case MEDIA_STOPPED: + { + TimeProvider timeProvider = mTimeProvider; + if (timeProvider != null) { + timeProvider.onStopped(); + } + } + break; + + case MEDIA_STARTED: + case MEDIA_PAUSED: + { + TimeProvider timeProvider = mTimeProvider; + if (timeProvider != null) { + timeProvider.onPaused(msg.what == MEDIA_PAUSED); + } + } + break; + + case MEDIA_BUFFERING_UPDATE: + final int percent = msg.arg1; + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onBufferingUpdate( + mMediaPlayer, srcId, percent)); + } + } + return; + + case MEDIA_SEEK_COMPLETE: + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onInfo( + mMediaPlayer, srcId, MEDIA_INFO_COMPLETE_CALL_SEEK, 0)); + } + } + // fall through + + case MEDIA_SKIPPED: + { + TimeProvider timeProvider = mTimeProvider; + if (timeProvider != null) { + timeProvider.onSeekComplete(mMediaPlayer); + } + } + return; + + case MEDIA_SET_VIDEO_SIZE: + final int width = msg.arg1; + final int height = msg.arg2; + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onVideoSizeChanged( + mMediaPlayer, srcId, width, height)); + } + } + return; + + case MEDIA_ERROR: + Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")"); + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onError( + mMediaPlayer, srcId, what, extra)); + cb.first.execute(() -> cb.second.onInfo( + mMediaPlayer, srcId, MEDIA_INFO_PLAYBACK_COMPLETE, 0)); + } + } + stayAwake(false); + return; + + case MEDIA_INFO: + switch (msg.arg1) { + case MEDIA_INFO_VIDEO_TRACK_LAGGING: + Log.i(TAG, "Info (" + msg.arg1 + "," + msg.arg2 + ")"); + break; + + case MEDIA_INFO_METADATA_UPDATE: + try { + scanInternalSubtitleTracks(); + } catch (RuntimeException e) { + Message msg2 = obtainMessage( + MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, + null); + sendMessage(msg2); + } + // fall through + + case MEDIA_INFO_EXTERNAL_METADATA_UPDATE: + msg.arg1 = MEDIA_INFO_METADATA_UPDATE; + // update default track selection + if (mSubtitleController != null) { + mSubtitleController.selectDefaultTrack(); + } + break; + + case MEDIA_INFO_BUFFERING_START: + case MEDIA_INFO_BUFFERING_END: + TimeProvider timeProvider = mTimeProvider; + if (timeProvider != null) { + timeProvider.onBuffering(msg.arg1 == MEDIA_INFO_BUFFERING_START); + } + break; + } + + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onInfo( + mMediaPlayer, srcId, what, extra)); + } + } + // No real default action so far. + return; + + case MEDIA_NOTIFY_TIME: + TimeProvider timeProvider = mTimeProvider; + if (timeProvider != null) { + timeProvider.onNotifyTime(); + } + return; + + case MEDIA_TIMED_TEXT: + final TimedText text; + if (msg.obj instanceof Parcel) { + Parcel parcel = (Parcel)msg.obj; + text = new TimedText(parcel); + parcel.recycle(); + } else { + text = null; + } + + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onTimedText(mMediaPlayer, srcId, text)); + } + } + return; + + case MEDIA_SUBTITLE_DATA: + OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener; + if (onSubtitleDataListener == null) { + return; + } + if (msg.obj instanceof Parcel) { + Parcel parcel = (Parcel) msg.obj; + SubtitleData data = new SubtitleData(parcel); + parcel.recycle(); + onSubtitleDataListener.onSubtitleData(mMediaPlayer, data); + } + return; + + case MEDIA_META_DATA: + final TimedMetaData data; + if (msg.obj instanceof Parcel) { + Parcel parcel = (Parcel) msg.obj; + data = TimedMetaData.createTimedMetaDataFromParcel(parcel); + parcel.recycle(); + } else { + data = null; + } + + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + cb.first.execute(() -> cb.second.onTimedMetaDataAvailable( + mMediaPlayer, srcId, data)); + } + } + return; + + case MEDIA_NOP: // interface test message - ignore + break; + + case MEDIA_AUDIO_ROUTING_CHANGED: + AudioManager.resetAudioPortGeneration(); + synchronized (mRoutingChangeListeners) { + for (NativeRoutingEventHandlerDelegate delegate + : mRoutingChangeListeners.values()) { + delegate.notifyClient(); + } + } + return; + + default: + Log.e(TAG, "Unknown message type " + msg.what); + return; + } + } + } + + /* + * Called from native code when an interesting event happens. This method + * just uses the EventHandler system to post the event back to the main app thread. + * We use a weak reference to the original MediaPlayer2 object so that the native + * code is safe from the object disappearing from underneath it. (This is + * the cookie passed to native_setup().) + */ + private static void postEventFromNative(Object mediaplayer2_ref, long srcId, + int what, int arg1, int arg2, Object obj) + { + final MediaPlayer2Impl mp = (MediaPlayer2Impl)((WeakReference)mediaplayer2_ref).get(); + if (mp == null) { + return; + } + + switch (what) { + case MEDIA_INFO: + if (arg1 == MEDIA_INFO_STARTED_AS_NEXT) { + new Thread(new Runnable() { + @Override + public void run() { + // this acquires the wakelock if needed, and sets the client side state + mp.play(); + } + }).start(); + Thread.yield(); + } + break; + + case MEDIA_DRM_INFO: + // We need to derive mDrmInfoImpl before prepare() returns so processing it here + // before the notification is sent to EventHandler below. EventHandler runs in the + // notification looper so its handleMessage might process the event after prepare() + // has returned. + Log.v(TAG, "postEventFromNative MEDIA_DRM_INFO"); + if (obj instanceof Parcel) { + Parcel parcel = (Parcel)obj; + DrmInfoImpl drmInfo = new DrmInfoImpl(parcel); + synchronized (mp.mDrmLock) { + mp.mDrmInfoImpl = drmInfo; + } + } else { + Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + obj); + } + break; + + case MEDIA_PREPARED: + // By this time, we've learned about DrmInfo's presence or absence. This is meant + // mainly for prepareAsync() use case. For prepare(), this still can run to a race + // condition b/c MediaPlayerNative releases the prepare() lock before calling notify + // so we also set mDrmInfoResolved in prepare(). + synchronized (mp.mDrmLock) { + mp.mDrmInfoResolved = true; + } + break; + + } + + if (mp.mEventHandler != null) { + Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj); + + mp.mEventHandler.post(new Runnable() { + @Override + public void run() { + mp.mEventHandler.handleMessage(m, srcId); + } + }); + } + } + + private final Object mEventCbLock = new Object(); + private ArrayList<Pair<Executor, EventCallback> > mEventCallbackRecords + = new ArrayList<Pair<Executor, EventCallback> >(); + + /** + * Register a callback to be invoked when the media source is ready + * for playback. + * + * @param eventCallback the callback that will be run + * @param executor the executor through which the callback should be invoked + */ + @Override + public void registerEventCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull EventCallback eventCallback) { + if (eventCallback == null) { + throw new IllegalArgumentException("Illegal null EventCallback"); + } + if (executor == null) { + throw new IllegalArgumentException("Illegal null Executor for the EventCallback"); + } + synchronized (mEventCbLock) { + mEventCallbackRecords.add(new Pair(executor, eventCallback)); + } + } + + /** + * Unregisters an {@link EventCallback}. + * + * @param callback an {@link EventCallback} to unregister + */ + @Override + public void unregisterEventCallback(EventCallback callback) { + synchronized (mEventCbLock) { + for (Pair<Executor, EventCallback> cb : mEventCallbackRecords) { + if (cb.second == callback) { + mEventCallbackRecords.remove(cb); + } + } + } + } + + /** + * Register a callback to be invoked when a track has data available. + * + * @param listener the callback that will be run + * + * @hide + */ + @Override + public void setOnSubtitleDataListener(OnSubtitleDataListener listener) { + mOnSubtitleDataListener = listener; + } + + private OnSubtitleDataListener mOnSubtitleDataListener; + + + // Modular DRM begin + + /** + * Register a callback to be invoked for configuration of the DRM object before + * the session is created. + * The callback will be invoked synchronously during the execution + * of {@link #prepareDrm(UUID uuid)}. + * + * @param listener the callback that will be run + */ + @Override + public void setOnDrmConfigHelper(OnDrmConfigHelper listener) + { + synchronized (mDrmLock) { + mOnDrmConfigHelper = listener; + } // synchronized + } + + private OnDrmConfigHelper mOnDrmConfigHelper; + + private final Object mDrmEventCbLock = new Object(); + private ArrayList<Pair<Executor, DrmEventCallback> > mDrmEventCallbackRecords + = new ArrayList<Pair<Executor, DrmEventCallback> >(); + + /** + * Register a callback to be invoked when the media source is ready + * for playback. + * + * @param eventCallback the callback that will be run + * @param executor the executor through which the callback should be invoked + */ + @Override + public void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull DrmEventCallback eventCallback) { + if (eventCallback == null) { + throw new IllegalArgumentException("Illegal null EventCallback"); + } + if (executor == null) { + throw new IllegalArgumentException("Illegal null Executor for the EventCallback"); + } + synchronized (mDrmEventCbLock) { + mDrmEventCallbackRecords.add(new Pair(executor, eventCallback)); + } + } + + /** + * Unregisters a {@link DrmEventCallback}. + * + * @param callback a {@link DrmEventCallback} to unregister + */ + @Override + public void unregisterDrmEventCallback(DrmEventCallback callback) { + synchronized (mDrmEventCbLock) { + for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) { + if (cb.second == callback) { + mDrmEventCallbackRecords.remove(cb); + break; + } + } + } + } + + + /** + * Retrieves the DRM Info associated with the current source + * + * @throws IllegalStateException if called before prepare() + */ + @Override + public DrmInfo getDrmInfo() { + DrmInfoImpl drmInfo = null; + + // there is not much point if the app calls getDrmInfo within an OnDrmInfoListenet; + // regardless below returns drmInfo anyway instead of raising an exception + synchronized (mDrmLock) { + if (!mDrmInfoResolved && mDrmInfoImpl == null) { + final String msg = "The Player has not been prepared yet"; + Log.v(TAG, msg); + throw new IllegalStateException(msg); + } + + if (mDrmInfoImpl != null) { + drmInfo = mDrmInfoImpl.makeCopy(); + } + } // synchronized + + return drmInfo; + } + + + /** + * Prepares the DRM for the current source + * <p> + * If {@code OnDrmConfigHelper} is registered, it will be called during + * preparation to allow configuration of the DRM properties before opening the + * DRM session. Note that the callback is called synchronously in the thread that called + * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString} + * and {@code setDrmPropertyString} calls and refrain from any lengthy operation. + * <p> + * If the device has not been provisioned before, this call also provisions the device + * which involves accessing the provisioning server and can take a variable time to + * complete depending on the network connectivity. + * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking + * mode by launching the provisioning in the background and returning. The listener + * will be called when provisioning and preparation has finished. If a + * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning + * and preparation has finished, i.e., runs in blocking mode. + * <p> + * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM + * session being ready. The application should not make any assumption about its call + * sequence (e.g., before or after prepareDrm returns), or the thread context that will + * execute the listener (unless the listener is registered with a handler thread). + * <p> + * + * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved + * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}. + * + * @throws IllegalStateException if called before prepare(), or the DRM was + * prepared already + * @throws UnsupportedSchemeException if the crypto scheme is not supported + * @throws ResourceBusyException if required DRM resources are in use + * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a + * network error + * @throws ProvisioningServerErrorException if provisioning is required but failed due to + * the request denied by the provisioning server + */ + @Override + public void prepareDrm(@NonNull UUID uuid) + throws UnsupportedSchemeException, ResourceBusyException, + ProvisioningNetworkErrorException, ProvisioningServerErrorException + { + Log.v(TAG, "prepareDrm: uuid: " + uuid + " mOnDrmConfigHelper: " + mOnDrmConfigHelper); + + boolean allDoneWithoutProvisioning = false; + + synchronized (mDrmLock) { + + // only allowing if tied to a protected source; might relax for releasing offline keys + if (mDrmInfoImpl == null) { + final String msg = "prepareDrm(): Wrong usage: The player must be prepared and " + + "DRM info be retrieved before this call."; + Log.e(TAG, msg); + throw new IllegalStateException(msg); + } + + if (mActiveDrmScheme) { + final String msg = "prepareDrm(): Wrong usage: There is already " + + "an active DRM scheme with " + mDrmUUID; + Log.e(TAG, msg); + throw new IllegalStateException(msg); + } + + if (mPrepareDrmInProgress) { + final String msg = "prepareDrm(): Wrong usage: There is already " + + "a pending prepareDrm call."; + Log.e(TAG, msg); + throw new IllegalStateException(msg); + } + + if (mDrmProvisioningInProgress) { + final String msg = "prepareDrm(): Unexpectd: Provisioning is already in progress."; + Log.e(TAG, msg); + throw new IllegalStateException(msg); + } + + // shouldn't need this; just for safeguard + cleanDrmObj(); + + mPrepareDrmInProgress = true; + + try { + // only creating the DRM object to allow pre-openSession configuration + prepareDrm_createDrmStep(uuid); + } catch (Exception e) { + Log.w(TAG, "prepareDrm(): Exception ", e); + mPrepareDrmInProgress = false; + throw e; + } + + mDrmConfigAllowed = true; + } // synchronized + + + // call the callback outside the lock + if (mOnDrmConfigHelper != null) { + mOnDrmConfigHelper.onDrmConfig(this); + } + + synchronized (mDrmLock) { + mDrmConfigAllowed = false; + boolean earlyExit = false; + + try { + prepareDrm_openSessionStep(uuid); + + mDrmUUID = uuid; + mActiveDrmScheme = true; + + allDoneWithoutProvisioning = true; + } catch (IllegalStateException e) { + final String msg = "prepareDrm(): Wrong usage: The player must be " + + "in the prepared state to call prepareDrm()."; + Log.e(TAG, msg); + earlyExit = true; + throw new IllegalStateException(msg); + } catch (NotProvisionedException e) { + Log.w(TAG, "prepareDrm: NotProvisionedException"); + + // handle provisioning internally; it'll reset mPrepareDrmInProgress + int result = HandleProvisioninig(uuid); + + // if blocking mode, we're already done; + // if non-blocking mode, we attempted to launch background provisioning + if (result != PREPARE_DRM_STATUS_SUCCESS) { + earlyExit = true; + String msg; + + switch (result) { + case PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR: + msg = "prepareDrm: Provisioning was required but failed " + + "due to a network error."; + Log.e(TAG, msg); + throw new ProvisioningNetworkErrorExceptionImpl(msg); + + case PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR: + msg = "prepareDrm: Provisioning was required but the request " + + "was denied by the server."; + Log.e(TAG, msg); + throw new ProvisioningServerErrorExceptionImpl(msg); + + case PREPARE_DRM_STATUS_PREPARATION_ERROR: + default: // default for safeguard + msg = "prepareDrm: Post-provisioning preparation failed."; + Log.e(TAG, msg); + throw new IllegalStateException(msg); + } + } + // nothing else to do; + // if blocking or non-blocking, HandleProvisioninig does the re-attempt & cleanup + } catch (Exception e) { + Log.e(TAG, "prepareDrm: Exception " + e); + earlyExit = true; + throw e; + } finally { + if (!mDrmProvisioningInProgress) {// if early exit other than provisioning exception + mPrepareDrmInProgress = false; + } + if (earlyExit) { // cleaning up object if didn't succeed + cleanDrmObj(); + } + } // finally + } // synchronized + + + // if finished successfully without provisioning, call the callback outside the lock + if (allDoneWithoutProvisioning) { + synchronized (mDrmEventCbLock) { + for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) { + cb.first.execute(() -> cb.second.onDrmPrepared( + this, PREPARE_DRM_STATUS_SUCCESS)); + } + } + } + + } + + + private native void _releaseDrm(); + + /** + * Releases the DRM session + * <p> + * The player has to have an active DRM session and be in stopped, or prepared + * state before this call is made. + * A {@code reset()} call will release the DRM session implicitly. + * + * @throws NoDrmSchemeException if there is no active DRM session to release + */ + @Override + public void releaseDrm() + throws NoDrmSchemeException + { + Log.v(TAG, "releaseDrm:"); + + synchronized (mDrmLock) { + if (!mActiveDrmScheme) { + Log.e(TAG, "releaseDrm(): No active DRM scheme to release."); + throw new NoDrmSchemeExceptionImpl("releaseDrm: No active DRM scheme to release."); + } + + try { + // we don't have the player's state in this layer. The below call raises + // exception if we're in a non-stopped/prepared state. + + // for cleaning native/mediaserver crypto object + _releaseDrm(); + + // for cleaning client-side MediaDrm object; only called if above has succeeded + cleanDrmObj(); + + mActiveDrmScheme = false; + } catch (IllegalStateException e) { + Log.w(TAG, "releaseDrm: Exception ", e); + throw new IllegalStateException("releaseDrm: The player is not in a valid state."); + } catch (Exception e) { + Log.e(TAG, "releaseDrm: Exception ", e); + } + } // synchronized + } + + + /** + * A key request/response exchange occurs between the app and a license server + * to obtain or release keys used to decrypt encrypted content. + * <p> + * getKeyRequest() is used to obtain an opaque key request byte array that is + * delivered to the license server. The opaque key request byte array is returned + * in KeyRequest.data. The recommended URL to deliver the key request to is + * returned in KeyRequest.defaultUrl. + * <p> + * After the app has received the key request response from the server, + * it should deliver to the response to the DRM engine plugin using the method + * {@link #provideKeyResponse}. + * + * @param keySetId is the key-set identifier of the offline keys being released when keyType is + * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when + * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. + * + * @param initData is the container-specific initialization data when the keyType is + * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is + * interpreted based on the mime type provided in the mimeType parameter. It could + * contain, for example, the content ID, key ID or other data obtained from the content + * metadata that is required in generating the key request. + * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null. + * + * @param mimeType identifies the mime type of the content + * + * @param keyType specifies the type of the request. The request may be to acquire + * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content + * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired + * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId. + * + * @param optionalParameters are included in the key request message to + * allow a client application to provide additional message parameters to the server. + * This may be {@code null} if no additional parameters are to be sent. + * + * @throws NoDrmSchemeException if there is no active DRM session + */ + @Override + @NonNull + public MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData, + @Nullable String mimeType, @MediaDrm.KeyType int keyType, + @Nullable Map<String, String> optionalParameters) + throws NoDrmSchemeException + { + Log.v(TAG, "getKeyRequest: " + + " keySetId: " + keySetId + " initData:" + initData + " mimeType: " + mimeType + + " keyType: " + keyType + " optionalParameters: " + optionalParameters); + + synchronized (mDrmLock) { + if (!mActiveDrmScheme) { + Log.e(TAG, "getKeyRequest NoDrmSchemeException"); + throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first."); + } + + try { + byte[] scope = (keyType != MediaDrm.KEY_TYPE_RELEASE) ? + mDrmSessionId : // sessionId for KEY_TYPE_STREAMING/OFFLINE + keySetId; // keySetId for KEY_TYPE_RELEASE + + HashMap<String, String> hmapOptionalParameters = + (optionalParameters != null) ? + new HashMap<String, String>(optionalParameters) : + null; + + MediaDrm.KeyRequest request = mDrmObj.getKeyRequest(scope, initData, mimeType, + keyType, hmapOptionalParameters); + Log.v(TAG, "getKeyRequest: --> request: " + request); + + return request; + + } catch (NotProvisionedException e) { + Log.w(TAG, "getKeyRequest NotProvisionedException: " + + "Unexpected. Shouldn't have reached here."); + throw new IllegalStateException("getKeyRequest: Unexpected provisioning error."); + } catch (Exception e) { + Log.w(TAG, "getKeyRequest Exception " + e); + throw e; + } + + } // synchronized + } + + + /** + * A key response is received from the license server by the app, then it is + * provided to the DRM engine plugin using provideKeyResponse. When the + * response is for an offline key request, a key-set identifier is returned that + * can be used to later restore the keys to a new session with the method + * {@ link # restoreKeys}. + * When the response is for a streaming or release request, null is returned. + * + * @param keySetId When the response is for a release request, keySetId identifies + * the saved key associated with the release request (i.e., the same keySetId + * passed to the earlier {@ link # getKeyRequest} call. It MUST be null when the + * response is for either streaming or offline key requests. + * + * @param response the byte array response from the server + * + * @throws NoDrmSchemeException if there is no active DRM session + * @throws DeniedByServerException if the response indicates that the + * server rejected the request + */ + @Override + public byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response) + throws NoDrmSchemeException, DeniedByServerException + { + Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response); + + synchronized (mDrmLock) { + + if (!mActiveDrmScheme) { + Log.e(TAG, "getKeyRequest NoDrmSchemeException"); + throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first."); + } + + try { + byte[] scope = (keySetId == null) ? + mDrmSessionId : // sessionId for KEY_TYPE_STREAMING/OFFLINE + keySetId; // keySetId for KEY_TYPE_RELEASE + + byte[] keySetResult = mDrmObj.provideKeyResponse(scope, response); + + Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response + + " --> " + keySetResult); + + + return keySetResult; + + } catch (NotProvisionedException e) { + Log.w(TAG, "provideKeyResponse NotProvisionedException: " + + "Unexpected. Shouldn't have reached here."); + throw new IllegalStateException("provideKeyResponse: " + + "Unexpected provisioning error."); + } catch (Exception e) { + Log.w(TAG, "provideKeyResponse Exception " + e); + throw e; + } + } // synchronized + } + + + /** + * Restore persisted offline keys into a new session. keySetId identifies the + * keys to load, obtained from a prior call to {@link #provideKeyResponse}. + * + * @param keySetId identifies the saved key set to restore + */ + @Override + public void restoreKeys(@NonNull byte[] keySetId) + throws NoDrmSchemeException + { + Log.v(TAG, "restoreKeys: keySetId: " + keySetId); + + synchronized (mDrmLock) { + + if (!mActiveDrmScheme) { + Log.w(TAG, "restoreKeys NoDrmSchemeException"); + throw new NoDrmSchemeExceptionImpl("restoreKeys: Has to set a DRM scheme first."); + } + + try { + mDrmObj.restoreKeys(mDrmSessionId, keySetId); + } catch (Exception e) { + Log.w(TAG, "restoreKeys Exception " + e); + throw e; + } + + } // synchronized + } + + + /** + * Read a DRM engine plugin String property value, given the property name string. + * <p> + * @param propertyName the property name + * + * Standard fields names are: + * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION}, + * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS} + */ + @Override + @NonNull + public String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName) + throws NoDrmSchemeException + { + Log.v(TAG, "getDrmPropertyString: propertyName: " + propertyName); + + String value; + synchronized (mDrmLock) { + + if (!mActiveDrmScheme && !mDrmConfigAllowed) { + Log.w(TAG, "getDrmPropertyString NoDrmSchemeException"); + throw new NoDrmSchemeExceptionImpl("getDrmPropertyString: Has to prepareDrm() first."); + } + + try { + value = mDrmObj.getPropertyString(propertyName); + } catch (Exception e) { + Log.w(TAG, "getDrmPropertyString Exception " + e); + throw e; + } + } // synchronized + + Log.v(TAG, "getDrmPropertyString: propertyName: " + propertyName + " --> value: " + value); + + return value; + } + + + /** + * Set a DRM engine plugin String property value. + * <p> + * @param propertyName the property name + * @param value the property value + * + * Standard fields names are: + * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION}, + * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS} + */ + @Override + public void setDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName, + @NonNull String value) + throws NoDrmSchemeException + { + Log.v(TAG, "setDrmPropertyString: propertyName: " + propertyName + " value: " + value); + + synchronized (mDrmLock) { + + if ( !mActiveDrmScheme && !mDrmConfigAllowed ) { + Log.w(TAG, "setDrmPropertyString NoDrmSchemeException"); + throw new NoDrmSchemeExceptionImpl("setDrmPropertyString: Has to prepareDrm() first."); + } + + try { + mDrmObj.setPropertyString(propertyName, value); + } catch ( Exception e ) { + Log.w(TAG, "setDrmPropertyString Exception " + e); + throw e; + } + } // synchronized + } + + /** + * Encapsulates the DRM properties of the source. + */ + public static final class DrmInfoImpl extends DrmInfo { + private Map<UUID, byte[]> mapPssh; + private UUID[] supportedSchemes; + + /** + * Returns the PSSH info of the data source for each supported DRM scheme. + */ + @Override + public Map<UUID, byte[]> getPssh() { + return mapPssh; + } + + /** + * Returns the intersection of the data source and the device DRM schemes. + * It effectively identifies the subset of the source's DRM schemes which + * are supported by the device too. + */ + @Override + public List<UUID> getSupportedSchemes() { + return Arrays.asList(supportedSchemes); + } + + private DrmInfoImpl(Map<UUID, byte[]> Pssh, UUID[] SupportedSchemes) { + mapPssh = Pssh; + supportedSchemes = SupportedSchemes; + } + + private DrmInfoImpl(Parcel parcel) { + Log.v(TAG, "DrmInfoImpl(" + parcel + ") size " + parcel.dataSize()); + + int psshsize = parcel.readInt(); + byte[] pssh = new byte[psshsize]; + parcel.readByteArray(pssh); + + Log.v(TAG, "DrmInfoImpl() PSSH: " + arrToHex(pssh)); + mapPssh = parsePSSH(pssh, psshsize); + Log.v(TAG, "DrmInfoImpl() PSSH: " + mapPssh); + + int supportedDRMsCount = parcel.readInt(); + supportedSchemes = new UUID[supportedDRMsCount]; + for (int i = 0; i < supportedDRMsCount; i++) { + byte[] uuid = new byte[16]; + parcel.readByteArray(uuid); + + supportedSchemes[i] = bytesToUUID(uuid); + + Log.v(TAG, "DrmInfoImpl() supportedScheme[" + i + "]: " + + supportedSchemes[i]); + } + + Log.v(TAG, "DrmInfoImpl() Parcel psshsize: " + psshsize + + " supportedDRMsCount: " + supportedDRMsCount); + } + + private DrmInfoImpl makeCopy() { + return new DrmInfoImpl(this.mapPssh, this.supportedSchemes); + } + + private String arrToHex(byte[] bytes) { + String out = "0x"; + for (int i = 0; i < bytes.length; i++) { + out += String.format("%02x", bytes[i]); + } + + return out; + } + + private UUID bytesToUUID(byte[] uuid) { + long msb = 0, lsb = 0; + for (int i = 0; i < 8; i++) { + msb |= ( ((long)uuid[i] & 0xff) << (8 * (7 - i)) ); + lsb |= ( ((long)uuid[i+8] & 0xff) << (8 * (7 - i)) ); + } + + return new UUID(msb, lsb); + } + + private Map<UUID, byte[]> parsePSSH(byte[] pssh, int psshsize) { + Map<UUID, byte[]> result = new HashMap<UUID, byte[]>(); + + final int UUID_SIZE = 16; + final int DATALEN_SIZE = 4; + + int len = psshsize; + int numentries = 0; + int i = 0; + + while (len > 0) { + if (len < UUID_SIZE) { + Log.w(TAG, String.format("parsePSSH: len is too short to parse " + + "UUID: (%d < 16) pssh: %d", len, psshsize)); + return null; + } + + byte[] subset = Arrays.copyOfRange(pssh, i, i + UUID_SIZE); + UUID uuid = bytesToUUID(subset); + i += UUID_SIZE; + len -= UUID_SIZE; + + // get data length + if (len < 4) { + Log.w(TAG, String.format("parsePSSH: len is too short to parse " + + "datalen: (%d < 4) pssh: %d", len, psshsize)); + return null; + } + + subset = Arrays.copyOfRange(pssh, i, i+DATALEN_SIZE); + int datalen = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) ? + ((subset[3] & 0xff) << 24) | ((subset[2] & 0xff) << 16) | + ((subset[1] & 0xff) << 8) | (subset[0] & 0xff) : + ((subset[0] & 0xff) << 24) | ((subset[1] & 0xff) << 16) | + ((subset[2] & 0xff) << 8) | (subset[3] & 0xff) ; + i += DATALEN_SIZE; + len -= DATALEN_SIZE; + + if (len < datalen) { + Log.w(TAG, String.format("parsePSSH: len is too short to parse " + + "data: (%d < %d) pssh: %d", len, datalen, psshsize)); + return null; + } + + byte[] data = Arrays.copyOfRange(pssh, i, i+datalen); + + // skip the data + i += datalen; + len -= datalen; + + Log.v(TAG, String.format("parsePSSH[%d]: <%s, %s> pssh: %d", + numentries, uuid, arrToHex(data), psshsize)); + numentries++; + result.put(uuid, data); + } + + return result; + } + + }; // DrmInfoImpl + + /** + * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm(). + * Extends MediaDrm.MediaDrmException + */ + public static final class NoDrmSchemeExceptionImpl extends NoDrmSchemeException { + public NoDrmSchemeExceptionImpl(String detailMessage) { + super(detailMessage); + } + } + + /** + * Thrown when the device requires DRM provisioning but the provisioning attempt has + * failed due to a network error (Internet reachability, timeout, etc.). + * Extends MediaDrm.MediaDrmException + */ + public static final class ProvisioningNetworkErrorExceptionImpl + extends ProvisioningNetworkErrorException { + public ProvisioningNetworkErrorExceptionImpl(String detailMessage) { + super(detailMessage); + } + } + + /** + * Thrown when the device requires DRM provisioning but the provisioning attempt has + * failed due to the provisioning server denying the request. + * Extends MediaDrm.MediaDrmException + */ + public static final class ProvisioningServerErrorExceptionImpl + extends ProvisioningServerErrorException { + public ProvisioningServerErrorExceptionImpl(String detailMessage) { + super(detailMessage); + } + } + + + private native void _prepareDrm(@NonNull byte[] uuid, @NonNull byte[] drmSessionId); + + // Modular DRM helpers + + private void prepareDrm_createDrmStep(@NonNull UUID uuid) + throws UnsupportedSchemeException { + Log.v(TAG, "prepareDrm_createDrmStep: UUID: " + uuid); + + try { + mDrmObj = new MediaDrm(uuid); + Log.v(TAG, "prepareDrm_createDrmStep: Created mDrmObj=" + mDrmObj); + } catch (Exception e) { // UnsupportedSchemeException + Log.e(TAG, "prepareDrm_createDrmStep: MediaDrm failed with " + e); + throw e; + } + } + + private void prepareDrm_openSessionStep(@NonNull UUID uuid) + throws NotProvisionedException, ResourceBusyException { + Log.v(TAG, "prepareDrm_openSessionStep: uuid: " + uuid); + + // TODO: don't need an open session for a future specialKeyReleaseDrm mode but we should do + // it anyway so it raises provisioning error if needed. We'd rather handle provisioning + // at prepareDrm/openSession rather than getKeyRequest/provideKeyResponse + try { + mDrmSessionId = mDrmObj.openSession(); + Log.v(TAG, "prepareDrm_openSessionStep: mDrmSessionId=" + mDrmSessionId); + + // Sending it down to native/mediaserver to create the crypto object + // This call could simply fail due to bad player state, e.g., after play(). + _prepareDrm(getByteArrayFromUUID(uuid), mDrmSessionId); + Log.v(TAG, "prepareDrm_openSessionStep: _prepareDrm/Crypto succeeded"); + + } catch (Exception e) { //ResourceBusyException, NotProvisionedException + Log.e(TAG, "prepareDrm_openSessionStep: open/crypto failed with " + e); + throw e; + } + + } + + // Called from the native side + @SuppressWarnings("unused") + private static boolean setAudioOutputDeviceById(AudioTrack track, int deviceId) { + if (track == null) { + return false; + } + + if (deviceId == 0) { + // Use default routing. + track.setPreferredDevice(null); + return true; + } + + // TODO: Unhide AudioManager.getDevicesStatic. + AudioDeviceInfo[] outputDevices = + AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS); + + boolean success = false; + for (AudioDeviceInfo device : outputDevices) { + if (device.getId() == deviceId) { + track.setPreferredDevice(device); + success = true; + break; + } + } + return success; + } + + // Instantiated from the native side + @SuppressWarnings("unused") + private static class StreamEventCallback extends AudioTrack.StreamEventCallback { + public long mJAudioTrackPtr; + public long mNativeCallbackPtr; + public long mUserDataPtr; + + public StreamEventCallback(long jAudioTrackPtr, long nativeCallbackPtr, long userDataPtr) { + super(); + mJAudioTrackPtr = jAudioTrackPtr; + mNativeCallbackPtr = nativeCallbackPtr; + mUserDataPtr = userDataPtr; + } + + @Override + public void onTearDown(AudioTrack track) { + native_stream_event_onTearDown(mNativeCallbackPtr, mUserDataPtr); + } + + @Override + public void onStreamPresentationEnd(AudioTrack track) { + native_stream_event_onStreamPresentationEnd(mNativeCallbackPtr, mUserDataPtr); + } + + @Override + public void onStreamDataRequest(AudioTrack track) { + native_stream_event_onStreamDataRequest( + mJAudioTrackPtr, mNativeCallbackPtr, mUserDataPtr); + } + } + + private class ProvisioningThread extends Thread { + public static final int TIMEOUT_MS = 60000; + + private UUID uuid; + private String urlStr; + private Object drmLock; + private MediaPlayer2Impl mediaPlayer; + private int status; + private boolean finished; + public int status() { + return status; + } + + public ProvisioningThread initialize(MediaDrm.ProvisionRequest request, + UUID uuid, MediaPlayer2Impl mediaPlayer) { + // lock is held by the caller + drmLock = mediaPlayer.mDrmLock; + this.mediaPlayer = mediaPlayer; + + urlStr = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + this.uuid = uuid; + + status = PREPARE_DRM_STATUS_PREPARATION_ERROR; + + Log.v(TAG, "HandleProvisioninig: Thread is initialised url: " + urlStr); + return this; + } + + public void run() { + + byte[] response = null; + boolean provisioningSucceeded = false; + try { + URL url = new URL(urlStr); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + try { + connection.setRequestMethod("POST"); + connection.setDoOutput(false); + connection.setDoInput(true); + connection.setConnectTimeout(TIMEOUT_MS); + connection.setReadTimeout(TIMEOUT_MS); + + connection.connect(); + response = Streams.readFully(connection.getInputStream()); + + Log.v(TAG, "HandleProvisioninig: Thread run: response " + + response.length + " " + response); + } catch (Exception e) { + status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR; + Log.w(TAG, "HandleProvisioninig: Thread run: connect " + e + " url: " + url); + } finally { + connection.disconnect(); + } + } catch (Exception e) { + status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR; + Log.w(TAG, "HandleProvisioninig: Thread run: openConnection " + e); + } + + if (response != null) { + try { + mDrmObj.provideProvisionResponse(response); + Log.v(TAG, "HandleProvisioninig: Thread run: " + + "provideProvisionResponse SUCCEEDED!"); + + provisioningSucceeded = true; + } catch (Exception e) { + status = PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR; + Log.w(TAG, "HandleProvisioninig: Thread run: " + + "provideProvisionResponse " + e); + } + } + + boolean succeeded = false; + + boolean hasCallback = false; + synchronized (mDrmEventCbLock) { + hasCallback = !mDrmEventCallbackRecords.isEmpty(); + } + // non-blocking mode needs the lock + if (hasCallback) { + + synchronized (drmLock) { + // continuing with prepareDrm + if (provisioningSucceeded) { + succeeded = mediaPlayer.resumePrepareDrm(uuid); + status = (succeeded) ? + PREPARE_DRM_STATUS_SUCCESS : + PREPARE_DRM_STATUS_PREPARATION_ERROR; + } + mediaPlayer.mDrmProvisioningInProgress = false; + mediaPlayer.mPrepareDrmInProgress = false; + if (!succeeded) { + cleanDrmObj(); // cleaning up if it hasn't gone through while in the lock + } + } // synchronized + + // calling the callback outside the lock + synchronized (mDrmEventCbLock) { + for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) { + cb.first.execute(() -> cb.second.onDrmPrepared(mediaPlayer, status)); + } + } + } else { // blocking mode already has the lock + + // continuing with prepareDrm + if (provisioningSucceeded) { + succeeded = mediaPlayer.resumePrepareDrm(uuid); + status = (succeeded) ? + PREPARE_DRM_STATUS_SUCCESS : + PREPARE_DRM_STATUS_PREPARATION_ERROR; + } + mediaPlayer.mDrmProvisioningInProgress = false; + mediaPlayer.mPrepareDrmInProgress = false; + if (!succeeded) { + cleanDrmObj(); // cleaning up if it hasn't gone through + } + } + + finished = true; + } // run() + + } // ProvisioningThread + + private int HandleProvisioninig(UUID uuid) { + // the lock is already held by the caller + + if (mDrmProvisioningInProgress) { + Log.e(TAG, "HandleProvisioninig: Unexpected mDrmProvisioningInProgress"); + return PREPARE_DRM_STATUS_PREPARATION_ERROR; + } + + MediaDrm.ProvisionRequest provReq = mDrmObj.getProvisionRequest(); + if (provReq == null) { + Log.e(TAG, "HandleProvisioninig: getProvisionRequest returned null."); + return PREPARE_DRM_STATUS_PREPARATION_ERROR; + } + + Log.v(TAG, "HandleProvisioninig provReq " + + " data: " + provReq.getData() + " url: " + provReq.getDefaultUrl()); + + // networking in a background thread + mDrmProvisioningInProgress = true; + + mDrmProvisioningThread = new ProvisioningThread().initialize(provReq, uuid, this); + mDrmProvisioningThread.start(); + + int result; + + // non-blocking: this is not the final result + boolean hasCallback = false; + synchronized (mDrmEventCbLock) { + hasCallback = !mDrmEventCallbackRecords.isEmpty(); + } + if (hasCallback) { + result = PREPARE_DRM_STATUS_SUCCESS; + } else { + // if blocking mode, wait till provisioning is done + try { + mDrmProvisioningThread.join(); + } catch (Exception e) { + Log.w(TAG, "HandleProvisioninig: Thread.join Exception " + e); + } + result = mDrmProvisioningThread.status(); + // no longer need the thread + mDrmProvisioningThread = null; + } + + return result; + } + + private boolean resumePrepareDrm(UUID uuid) { + Log.v(TAG, "resumePrepareDrm: uuid: " + uuid); + + // mDrmLock is guaranteed to be held + boolean success = false; + try { + // resuming + prepareDrm_openSessionStep(uuid); + + mDrmUUID = uuid; + mActiveDrmScheme = true; + + success = true; + } catch (Exception e) { + Log.w(TAG, "HandleProvisioninig: Thread run _prepareDrm resume failed with " + e); + // mDrmObj clean up is done by the caller + } + + return success; + } + + private void resetDrmState() { + synchronized (mDrmLock) { + Log.v(TAG, "resetDrmState: " + + " mDrmInfoImpl=" + mDrmInfoImpl + + " mDrmProvisioningThread=" + mDrmProvisioningThread + + " mPrepareDrmInProgress=" + mPrepareDrmInProgress + + " mActiveDrmScheme=" + mActiveDrmScheme); + + mDrmInfoResolved = false; + mDrmInfoImpl = null; + + if (mDrmProvisioningThread != null) { + // timeout; relying on HttpUrlConnection + try { + mDrmProvisioningThread.join(); + } + catch (InterruptedException e) { + Log.w(TAG, "resetDrmState: ProvThread.join Exception " + e); + } + mDrmProvisioningThread = null; + } + + mPrepareDrmInProgress = false; + mActiveDrmScheme = false; + + cleanDrmObj(); + } // synchronized + } + + private void cleanDrmObj() { + // the caller holds mDrmLock + Log.v(TAG, "cleanDrmObj: mDrmObj=" + mDrmObj + " mDrmSessionId=" + mDrmSessionId); + + if (mDrmSessionId != null) { + mDrmObj.closeSession(mDrmSessionId); + mDrmSessionId = null; + } + if (mDrmObj != null) { + mDrmObj.release(); + mDrmObj = null; + } + } + + private static final byte[] getByteArrayFromUUID(@NonNull UUID uuid) { + long msb = uuid.getMostSignificantBits(); + long lsb = uuid.getLeastSignificantBits(); + + byte[] uuidBytes = new byte[16]; + for (int i = 0; i < 8; ++i) { + uuidBytes[i] = (byte)(msb >>> (8 * (7 - i))); + uuidBytes[8 + i] = (byte)(lsb >>> (8 * (7 - i))); + } + + return uuidBytes; + } + + // Modular DRM end + + /* + * Test whether a given video scaling mode is supported. + */ + private boolean isVideoScalingModeSupported(int mode) { + return (mode == VIDEO_SCALING_MODE_SCALE_TO_FIT || + mode == VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING); + } + + /** @hide */ + static class TimeProvider implements MediaTimeProvider { + private static final String TAG = "MTP"; + private static final long MAX_NS_WITHOUT_POSITION_CHECK = 5000000000L; + private static final long MAX_EARLY_CALLBACK_US = 1000; + private static final long TIME_ADJUSTMENT_RATE = 2; /* meaning 1/2 */ + private long mLastTimeUs = 0; + private MediaPlayer2Impl mPlayer; + private boolean mPaused = true; + private boolean mStopped = true; + private boolean mBuffering; + private long mLastReportedTime; + // since we are expecting only a handful listeners per stream, there is + // no need for log(N) search performance + private MediaTimeProvider.OnMediaTimeListener mListeners[]; + private long mTimes[]; + private EventHandler mEventHandler; + private boolean mRefresh = false; + private boolean mPausing = false; + private boolean mSeeking = false; + private static final int NOTIFY = 1; + private static final int NOTIFY_TIME = 0; + private static final int NOTIFY_STOP = 2; + private static final int NOTIFY_SEEK = 3; + private static final int NOTIFY_TRACK_DATA = 4; + private HandlerThread mHandlerThread; + + /** @hide */ + public boolean DEBUG = false; + + public TimeProvider(MediaPlayer2Impl mp) { + mPlayer = mp; + try { + getCurrentTimeUs(true, false); + } catch (IllegalStateException e) { + // we assume starting position + mRefresh = true; + } + + Looper looper; + if ((looper = Looper.myLooper()) == null && + (looper = Looper.getMainLooper()) == null) { + // Create our own looper here in case MP was created without one + mHandlerThread = new HandlerThread("MediaPlayer2MTPEventThread", + Process.THREAD_PRIORITY_FOREGROUND); + mHandlerThread.start(); + looper = mHandlerThread.getLooper(); + } + mEventHandler = new EventHandler(looper); + + mListeners = new MediaTimeProvider.OnMediaTimeListener[0]; + mTimes = new long[0]; + mLastTimeUs = 0; + } + + private void scheduleNotification(int type, long delayUs) { + // ignore time notifications until seek is handled + if (mSeeking && type == NOTIFY_TIME) { + return; + } + + if (DEBUG) Log.v(TAG, "scheduleNotification " + type + " in " + delayUs); + mEventHandler.removeMessages(NOTIFY); + Message msg = mEventHandler.obtainMessage(NOTIFY, type, 0); + mEventHandler.sendMessageDelayed(msg, (int) (delayUs / 1000)); + } + + /** @hide */ + public void close() { + mEventHandler.removeMessages(NOTIFY); + if (mHandlerThread != null) { + mHandlerThread.quitSafely(); + mHandlerThread = null; + } + } + + /** @hide */ + protected void finalize() { + if (mHandlerThread != null) { + mHandlerThread.quitSafely(); + } + } + + /** @hide */ + public void onNotifyTime() { + synchronized (this) { + if (DEBUG) Log.d(TAG, "onNotifyTime: "); + scheduleNotification(NOTIFY_TIME, 0 /* delay */); + } + } + + /** @hide */ + public void onPaused(boolean paused) { + synchronized(this) { + if (DEBUG) Log.d(TAG, "onPaused: " + paused); + if (mStopped) { // handle as seek if we were stopped + mStopped = false; + mSeeking = true; + scheduleNotification(NOTIFY_SEEK, 0 /* delay */); + } else { + mPausing = paused; // special handling if player disappeared + mSeeking = false; + scheduleNotification(NOTIFY_TIME, 0 /* delay */); + } + } + } + + /** @hide */ + public void onBuffering(boolean buffering) { + synchronized (this) { + if (DEBUG) Log.d(TAG, "onBuffering: " + buffering); + mBuffering = buffering; + scheduleNotification(NOTIFY_TIME, 0 /* delay */); + } + } + + /** @hide */ + public void onStopped() { + synchronized(this) { + if (DEBUG) Log.d(TAG, "onStopped"); + mPaused = true; + mStopped = true; + mSeeking = false; + mBuffering = false; + scheduleNotification(NOTIFY_STOP, 0 /* delay */); + } + } + + /** @hide */ + public void onSeekComplete(MediaPlayer2Impl mp) { + synchronized(this) { + mStopped = false; + mSeeking = true; + scheduleNotification(NOTIFY_SEEK, 0 /* delay */); + } + } + + /** @hide */ + public void onNewPlayer() { + if (mRefresh) { + synchronized(this) { + mStopped = false; + mSeeking = true; + mBuffering = false; + scheduleNotification(NOTIFY_SEEK, 0 /* delay */); + } + } + } + + private synchronized void notifySeek() { + mSeeking = false; + try { + long timeUs = getCurrentTimeUs(true, false); + if (DEBUG) Log.d(TAG, "onSeekComplete at " + timeUs); + + for (MediaTimeProvider.OnMediaTimeListener listener: mListeners) { + if (listener == null) { + break; + } + listener.onSeek(timeUs); + } + } catch (IllegalStateException e) { + // we should not be there, but at least signal pause + if (DEBUG) Log.d(TAG, "onSeekComplete but no player"); + mPausing = true; // special handling if player disappeared + notifyTimedEvent(false /* refreshTime */); + } + } + + private synchronized void notifyTrackData(Pair<SubtitleTrack, byte[]> trackData) { + SubtitleTrack track = trackData.first; + byte[] data = trackData.second; + track.onData(data, true /* eos */, ~0 /* runID: keep forever */); + } + + private synchronized void notifyStop() { + for (MediaTimeProvider.OnMediaTimeListener listener: mListeners) { + if (listener == null) { + break; + } + listener.onStop(); + } + } + + private int registerListener(MediaTimeProvider.OnMediaTimeListener listener) { + int i = 0; + for (; i < mListeners.length; i++) { + if (mListeners[i] == listener || mListeners[i] == null) { + break; + } + } + + // new listener + if (i >= mListeners.length) { + MediaTimeProvider.OnMediaTimeListener[] newListeners = + new MediaTimeProvider.OnMediaTimeListener[i + 1]; + long[] newTimes = new long[i + 1]; + System.arraycopy(mListeners, 0, newListeners, 0, mListeners.length); + System.arraycopy(mTimes, 0, newTimes, 0, mTimes.length); + mListeners = newListeners; + mTimes = newTimes; + } + + if (mListeners[i] == null) { + mListeners[i] = listener; + mTimes[i] = MediaTimeProvider.NO_TIME; + } + return i; + } + + public void notifyAt( + long timeUs, MediaTimeProvider.OnMediaTimeListener listener) { + synchronized(this) { + if (DEBUG) Log.d(TAG, "notifyAt " + timeUs); + mTimes[registerListener(listener)] = timeUs; + scheduleNotification(NOTIFY_TIME, 0 /* delay */); + } + } + + public void scheduleUpdate(MediaTimeProvider.OnMediaTimeListener listener) { + synchronized(this) { + if (DEBUG) Log.d(TAG, "scheduleUpdate"); + int i = registerListener(listener); + + if (!mStopped) { + mTimes[i] = 0; + scheduleNotification(NOTIFY_TIME, 0 /* delay */); + } + } + } + + public void cancelNotifications( + MediaTimeProvider.OnMediaTimeListener listener) { + synchronized(this) { + int i = 0; + for (; i < mListeners.length; i++) { + if (mListeners[i] == listener) { + System.arraycopy(mListeners, i + 1, + mListeners, i, mListeners.length - i - 1); + System.arraycopy(mTimes, i + 1, + mTimes, i, mTimes.length - i - 1); + mListeners[mListeners.length - 1] = null; + mTimes[mTimes.length - 1] = NO_TIME; + break; + } else if (mListeners[i] == null) { + break; + } + } + + scheduleNotification(NOTIFY_TIME, 0 /* delay */); + } + } + + private synchronized void notifyTimedEvent(boolean refreshTime) { + // figure out next callback + long nowUs; + try { + nowUs = getCurrentTimeUs(refreshTime, true); + } catch (IllegalStateException e) { + // assume we paused until new player arrives + mRefresh = true; + mPausing = true; // this ensures that call succeeds + nowUs = getCurrentTimeUs(refreshTime, true); + } + long nextTimeUs = nowUs; + + if (mSeeking) { + // skip timed-event notifications until seek is complete + return; + } + + if (DEBUG) { + StringBuilder sb = new StringBuilder(); + sb.append("notifyTimedEvent(").append(mLastTimeUs).append(" -> ") + .append(nowUs).append(") from {"); + boolean first = true; + for (long time: mTimes) { + if (time == NO_TIME) { + continue; + } + if (!first) sb.append(", "); + sb.append(time); + first = false; + } + sb.append("}"); + Log.d(TAG, sb.toString()); + } + + Vector<MediaTimeProvider.OnMediaTimeListener> activatedListeners = + new Vector<MediaTimeProvider.OnMediaTimeListener>(); + for (int ix = 0; ix < mTimes.length; ix++) { + if (mListeners[ix] == null) { + break; + } + if (mTimes[ix] <= NO_TIME) { + // ignore, unless we were stopped + } else if (mTimes[ix] <= nowUs + MAX_EARLY_CALLBACK_US) { + activatedListeners.add(mListeners[ix]); + if (DEBUG) Log.d(TAG, "removed"); + mTimes[ix] = NO_TIME; + } else if (nextTimeUs == nowUs || mTimes[ix] < nextTimeUs) { + nextTimeUs = mTimes[ix]; + } + } + + if (nextTimeUs > nowUs && !mPaused) { + // schedule callback at nextTimeUs + if (DEBUG) Log.d(TAG, "scheduling for " + nextTimeUs + " and " + nowUs); + mPlayer.notifyAt(nextTimeUs); + } else { + mEventHandler.removeMessages(NOTIFY); + // no more callbacks + } + + for (MediaTimeProvider.OnMediaTimeListener listener: activatedListeners) { + listener.onTimedEvent(nowUs); + } + } + + public long getCurrentTimeUs(boolean refreshTime, boolean monotonic) + throws IllegalStateException { + synchronized (this) { + // we always refresh the time when the paused-state changes, because + // we expect to have received the pause-change event delayed. + if (mPaused && !refreshTime) { + return mLastReportedTime; + } + + try { + mLastTimeUs = mPlayer.getCurrentPosition() * 1000L; + mPaused = !mPlayer.isPlaying() || mBuffering; + if (DEBUG) Log.v(TAG, (mPaused ? "paused" : "playing") + " at " + mLastTimeUs); + } catch (IllegalStateException e) { + if (mPausing) { + // if we were pausing, get last estimated timestamp + mPausing = false; + if (!monotonic || mLastReportedTime < mLastTimeUs) { + mLastReportedTime = mLastTimeUs; + } + mPaused = true; + if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime); + return mLastReportedTime; + } + // TODO get time when prepared + throw e; + } + if (monotonic && mLastTimeUs < mLastReportedTime) { + /* have to adjust time */ + if (mLastReportedTime - mLastTimeUs > 1000000) { + // schedule seeked event if time jumped significantly + // TODO: do this properly by introducing an exception + mStopped = false; + mSeeking = true; + scheduleNotification(NOTIFY_SEEK, 0 /* delay */); + } + } else { + mLastReportedTime = mLastTimeUs; + } + + return mLastReportedTime; + } + } + + private class EventHandler extends Handler { + public EventHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == NOTIFY) { + switch (msg.arg1) { + case NOTIFY_TIME: + notifyTimedEvent(true /* refreshTime */); + break; + case NOTIFY_STOP: + notifyStop(); + break; + case NOTIFY_SEEK: + notifySeek(); + break; + case NOTIFY_TRACK_DATA: + notifyTrackData((Pair<SubtitleTrack, byte[]>)msg.obj); + break; + } + } + } + } + } +} diff --git a/media/java/android/media/MediaPlayerInterface.java b/media/java/android/media/MediaPlayerInterface.java new file mode 100644 index 000000000000..78e2391455ac --- /dev/null +++ b/media/java/android/media/MediaPlayerInterface.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 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.annotation.NonNull; +import android.annotation.Nullable; +import android.media.MediaSession2.PlaylistParams; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Base interfaces for all media players that want media session. + * @hide + */ +public interface MediaPlayerInterface { + /** + * Listens change in {@link PlaybackState2}. + */ + interface PlaybackListener { + /** + * Called when {@link PlaybackState2} for this player is changed. + */ + void onPlaybackChanged(PlaybackState2 state); + } + + // Transport controls that session will send command directly to this player. + void play(); + void prepare(); + void pause(); + void stop(); + void skipToPrevious(); + void skipToNext(); + void seekTo(long pos); + void fastForward(); + void rewind(); + + PlaybackState2 getPlaybackState(); + + /** + * Sets the {@link AudioAttributes} to be used during the playback of the media. + * + * @param attributes non-null <code>AudioAttributes</code>. + */ + void setAudioAttributes(@NonNull AudioAttributes attributes); + + /** + * Returns AudioAttributes that media player has. + */ + @Nullable + AudioAttributes getAudioAttributes(); + + void addPlaylistItem(int index, MediaItem2 item); + void removePlaylistItem(MediaItem2 item); + + void setPlaylist(List<MediaItem2> playlist); + List<MediaItem2> getPlaylist(); + + void setCurrentPlaylistItem(int index); + void setPlaylistParams(PlaylistParams params); + PlaylistParams getPlaylistParams(); + + /** + * Add a {@link PlaybackListener} to be invoked when the playback state is changed. + * + * @param executor the Handler that will receive the listener + * @param listener the listener that will be run + */ + void addPlaybackListener(Executor executor, PlaybackListener listener); + + /** + * Remove previously added {@link PlaybackListener}. + * + * @param listener the listener to be removed + */ + void removePlaybackListener(PlaybackListener listener); +} diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java index 59a124fa434f..823410f6bb76 100644 --- a/media/java/android/media/MediaRecorder.java +++ b/media/java/android/media/MediaRecorder.java @@ -25,6 +25,8 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.PersistableBundle; +import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; import android.view.Surface; @@ -33,6 +35,10 @@ import java.io.FileDescriptor; import java.io.IOException; import java.io.RandomAccessFile; import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import com.android.internal.annotations.GuardedBy; /** * Used to record audio and video. The recording control is based on a @@ -76,7 +82,7 @@ import java.lang.ref.WeakReference; * <a href="{@docRoot}guide/topics/media/audio-capture.html">Audio Capture</a> developer guide.</p> * </div> */ -public class MediaRecorder +public class MediaRecorder implements AudioRouting { static { System.loadLibrary("media_jni"); @@ -917,7 +923,7 @@ public class MediaRecorder */ public void setNextOutputFile(File file) throws IOException { - RandomAccessFile f = new RandomAccessFile(file, "rws"); + RandomAccessFile f = new RandomAccessFile(file, "rw"); try { _setNextOutputFile(f.getFD()); } finally { @@ -942,7 +948,7 @@ public class MediaRecorder public void prepare() throws IllegalStateException, IOException { if (mPath != null) { - RandomAccessFile file = new RandomAccessFile(mPath, "rws"); + RandomAccessFile file = new RandomAccessFile(mPath, "rw"); try { _setOutputFile(file.getFD()); } finally { @@ -951,7 +957,7 @@ public class MediaRecorder } else if (mFd != null) { _setOutputFile(mFd); } else if (mFile != null) { - RandomAccessFile file = new RandomAccessFile(mFile, "rws"); + RandomAccessFile file = new RandomAccessFile(mFile, "rw"); try { _setOutputFile(file.getFD()); } finally { @@ -1243,6 +1249,7 @@ public class MediaRecorder private static final int MEDIA_RECORDER_TRACK_EVENT_INFO = 101; private static final int MEDIA_RECORDER_TRACK_EVENT_LIST_END = 1000; + private static final int MEDIA_RECORDER_AUDIO_ROUTING_CHANGED = 10000; @Override public void handleMessage(Message msg) { @@ -1265,6 +1272,16 @@ public class MediaRecorder return; + case MEDIA_RECORDER_AUDIO_ROUTING_CHANGED: + AudioManager.resetAudioPortGeneration(); + synchronized (mRoutingChangeListeners) { + for (NativeRoutingEventHandlerDelegate delegate + : mRoutingChangeListeners.values()) { + delegate.notifyClient(); + } + } + return; + default: Log.e(TAG, "Unknown message type " + msg.what); return; @@ -1272,6 +1289,152 @@ public class MediaRecorder } } + //-------------------------------------------------------------------------- + // Explicit Routing + //-------------------- + private AudioDeviceInfo mPreferredDevice = null; + + /** + * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route + * the input from this MediaRecorder. + * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio source. + * If deviceInfo is null, default routing is restored. + * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and + * does not correspond to a valid audio input device. + */ + @Override + public boolean setPreferredDevice(AudioDeviceInfo deviceInfo) { + if (deviceInfo != null && !deviceInfo.isSource()) { + return false; + } + int preferredDeviceId = deviceInfo != null ? deviceInfo.getId() : 0; + boolean status = native_setInputDevice(preferredDeviceId); + if (status == true) { + synchronized (this) { + mPreferredDevice = deviceInfo; + } + } + return status; + } + + /** + * Returns the selected input device specified by {@link #setPreferredDevice}. Note that this + * is not guaranteed to correspond to the actual device being used for recording. + */ + @Override + public AudioDeviceInfo getPreferredDevice() { + synchronized (this) { + return mPreferredDevice; + } + } + + /** + * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaRecorder + * Note: The query is only valid if the MediaRecorder is currently recording. + * If the recorder is not recording, the returned device can be null or correspond to previously + * selected device when the recorder was last active. + */ + @Override + public AudioDeviceInfo getRoutedDevice() { + int deviceId = native_getRoutedDeviceId(); + if (deviceId == 0) { + return null; + } + AudioDeviceInfo[] devices = + AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_INPUTS); + for (int i = 0; i < devices.length; i++) { + if (devices[i].getId() == deviceId) { + return devices[i]; + } + } + return null; + } + + /* + * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler. + */ + @GuardedBy("mRoutingChangeListeners") + private void enableNativeRoutingCallbacksLocked(boolean enabled) { + if (mRoutingChangeListeners.size() == 0) { + native_enableDeviceCallback(enabled); + } + } + + /** + * The list of AudioRouting.OnRoutingChangedListener interfaces added (with + * {@link #addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, Handler)} + * by an app to receive (re)routing notifications. + */ + @GuardedBy("mRoutingChangeListeners") + private ArrayMap<AudioRouting.OnRoutingChangedListener, + NativeRoutingEventHandlerDelegate> mRoutingChangeListeners = new ArrayMap<>(); + + /** + * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing + * changes on this MediaRecorder. + * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive + * notifications of rerouting events. + * @param handler Specifies the {@link Handler} object for the thread on which to execute + * the callback. If <code>null</code>, the handler on the main looper will be used. + */ + @Override + public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener, + Handler handler) { + synchronized (mRoutingChangeListeners) { + if (listener != null && !mRoutingChangeListeners.containsKey(listener)) { + enableNativeRoutingCallbacksLocked(true); + mRoutingChangeListeners.put( + listener, new NativeRoutingEventHandlerDelegate(this, listener, + handler != null ? handler : mEventHandler)); + } + } + } + + /** + * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added + * to receive rerouting notifications. + * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface + * to remove. + */ + @Override + public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener) { + synchronized (mRoutingChangeListeners) { + if (mRoutingChangeListeners.containsKey(listener)) { + mRoutingChangeListeners.remove(listener); + enableNativeRoutingCallbacksLocked(false); + } + } + } + + private native final boolean native_setInputDevice(int deviceId); + private native final int native_getRoutedDeviceId(); + private native final void native_enableDeviceCallback(boolean enabled); + + //-------------------------------------------------------------------------- + // Microphone information + //-------------------- + /** + * Return A lists of {@link MicrophoneInfo} representing the active microphones. + * By querying channel mapping for each active microphone, developer can know how + * the microphone is used by each channels or a capture stream. + * + * @return a lists of {@link MicrophoneInfo} representing the active microphones + * @throws IOException if an error occurs + */ + public List<MicrophoneInfo> getActiveMicrophones() throws IOException { + ArrayList<MicrophoneInfo> activeMicrophones = new ArrayList<>(); + int status = native_getActiveMicrophones(activeMicrophones); + if (status != AudioManager.SUCCESS) { + Log.e(TAG, "getActiveMicrophones failed:" + status); + return new ArrayList<MicrophoneInfo>(); + } + AudioManager.setPortIdForMicrophones(activeMicrophones); + return activeMicrophones; + } + + private native final int native_getActiveMicrophones( + ArrayList<MicrophoneInfo> activeMicrophones); + /** * Called from native code when an interesting event happens. This method * just uses the EventHandler system to post the event back to the main app thread. diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index b4fff4839e9c..70ab8632a889 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -194,8 +194,10 @@ public class MediaRouter { name = com.android.internal.R.string.default_audio_route_name_headphones; } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { name = com.android.internal.R.string.default_audio_route_name_dock_speakers; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { - name = com.android.internal.R.string.default_media_route_name_hdmi; + } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_HDMI) != 0) { + name = com.android.internal.R.string.default_audio_route_name_hdmi; + } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_USB) != 0) { + name = com.android.internal.R.string.default_audio_route_name_usb; } else { name = com.android.internal.R.string.default_audio_route_name; } diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java index cb4e46fe945a..c3090389aa37 100644 --- a/media/java/android/media/MediaScanner.java +++ b/media/java/android/media/MediaScanner.java @@ -158,6 +158,7 @@ public class MediaScanner implements AutoCloseable { public static final String SCANNED_BUILD_PREFS_NAME = "MediaScanBuild"; public static final String LAST_INTERNAL_SCAN_FINGERPRINT = "lastScanFingerprint"; private static final String SYSTEM_SOUNDS_DIR = "/system/media/audio"; + private static final String PRODUCT_SOUNDS_DIR = "/product/media/audio"; private static String sLastInternalScanFingerprint; private static final String[] ID3_GENRES = { @@ -1153,7 +1154,10 @@ public class MediaScanner implements AutoCloseable { private static boolean isSystemSoundWithMetadata(String path) { if (path.startsWith(SYSTEM_SOUNDS_DIR + ALARMS_DIR) || path.startsWith(SYSTEM_SOUNDS_DIR + RINGTONES_DIR) - || path.startsWith(SYSTEM_SOUNDS_DIR + NOTIFICATIONS_DIR)) { + || path.startsWith(SYSTEM_SOUNDS_DIR + NOTIFICATIONS_DIR) + || path.startsWith(PRODUCT_SOUNDS_DIR + ALARMS_DIR) + || path.startsWith(PRODUCT_SOUNDS_DIR + RINGTONES_DIR) + || path.startsWith(PRODUCT_SOUNDS_DIR + NOTIFICATIONS_DIR)) { return true; } return false; diff --git a/media/java/android/media/MediaSession2.java b/media/java/android/media/MediaSession2.java new file mode 100644 index 000000000000..943b827289f1 --- /dev/null +++ b/media/java/android/media/MediaSession2.java @@ -0,0 +1,1377 @@ +/* + * Copyright 2018 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.annotation.CallbackExecutor; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.MediaPlayerInterface.PlaybackListener; +import android.media.session.MediaSession; +import android.media.session.MediaSession.Callback; +import android.media.session.PlaybackState; +import android.media.update.ApiLoader; +import android.media.update.MediaSession2Provider; +import android.media.update.MediaSession2Provider.BuilderBaseProvider; +import android.media.update.MediaSession2Provider.CommandButtonProvider; +import android.media.update.MediaSession2Provider.CommandGroupProvider; +import android.media.update.MediaSession2Provider.CommandProvider; +import android.media.update.MediaSession2Provider.ControllerInfoProvider; +import android.media.update.ProviderCreator; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IInterface; +import android.os.ResultReceiver; +import android.text.TextUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Allows a media app to expose its transport controls and playback information in a process to + * other processes including the Android framework and other apps. Common use cases are as follows. + * <ul> + * <li>Bluetooth/wired headset key events support</li> + * <li>Android Auto/Wearable support</li> + * <li>Separating UI process and playback process</li> + * </ul> + * <p> + * A MediaSession2 should be created when an app wants to publish media playback information or + * handle media keys. In general an app only needs one session for all playback, though multiple + * sessions can be created to provide finer grain controls of media. + * <p> + * If you want to support background playback, {@link MediaSessionService2} is preferred + * instead. With it, your playback can be revived even after you've finished playback. See + * {@link MediaSessionService2} for details. + * <p> + * A session can be obtained by {@link Builder}. The owner of the session may pass its session token + * to other processes to allow them to create a {@link MediaController2} to interact with the + * session. + * <p> + * When a session receive transport control commands, the session sends the commands directly to + * the the underlying media player set by {@link Builder} or + * {@link #setPlayer(MediaPlayerInterface)}. + * <p> + * When an app is finished performing playback it must call {@link #close()} to clean up the session + * and notify any controllers. + * <p> + * {@link MediaSession2} objects should be used on the thread on the looper. + * + * @see MediaSessionService2 + * @hide + */ +public class MediaSession2 implements AutoCloseable { + private final MediaSession2Provider mProvider; + + // TODO(jaewan): Should we define IntDef? Currently we don't have to allow subclass to add more. + // TODO(jaewan): Shouldn't we pull out? + // TODO(jaewan): Should we also protect getters not related with metadata? + // Getters are getRatingType(), getPlaybackState(), getSessionActivity(), + // getPlaylistParams()) + // Next ID: 23 + /** + * Command code for the custom command which can be defined by string action in the + * {@link Command}. + */ + public static final int COMMAND_CODE_CUSTOM = 0; + + /** + * Command code for {@link MediaController2#play()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_PLAY = 1; + + /** + * Command code for {@link MediaController2#pause()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2; + + /** + * Command code for {@link MediaController2#stop()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_STOP = 3; + + /** + * Command code for {@link MediaController2#skipToNext()} ()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM = 4; + + /** + * Command code for {@link MediaController2#skipToPrevious()} ()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM = 5; + + /** + * Command code for {@link MediaController2#prepare()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6; + + /** + * Command code for {@link MediaController2#fastForward()} ()}. + * <p> + * This is transport control command. Command would be sent directly to the player if the + * session doesn't reject the request through the + * {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_FAST_FORWARD = 7; + + /** + * Command code for {@link MediaController2#rewind()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_REWIND = 8; + + /** + * Command code for {@link MediaController2#seekTo(long)} ()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9; + /** + * Command code for {@link MediaController2#setCurrentPlaylistItem(int)} ()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM = 10; + + /** + * Command code for {@link MediaController2#setPlaylistParams(PlaylistParams)} ()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS = 11; + + /** + * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYLIST_ADD = 12; + + /** + * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYLIST_REMOVE = 13; + + /** + * Command code for {@link MediaController2#getPlaylist()}. + * <p> + * Command would be sent directly to the player if the session doesn't reject the request + * through the {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_PLAYLIST_GET = 14; + + /** + * Command code for both {@link MediaController2#setVolumeTo(int, int)} and + * {@link MediaController2#adjustVolume(int, int)}. + * <p> + * Command would adjust the volume or sent to the volume provider directly if the session + * doesn't reject the request through the + * {@link SessionCallback#onCommandRequest(ControllerInfo, Command)}. + */ + public static final int COMMAND_CODE_SET_VOLUME = 15; + + /** + * Command code for {@link MediaController2#playFromMediaId(String, Bundle)}. + */ + public static final int COMMAND_CODE_PLAY_FROM_MEDIA_ID = 16; + + /** + * Command code for {@link MediaController2#playFromUri(String, Bundle)}. + */ + public static final int COMMAND_CODE_PLAY_FROM_URI = 17; + + /** + * Command code for {@link MediaController2#playFromSearch(String, Bundle)}. + */ + public static final int COMMAND_CODE_PLAY_FROM_SEARCH = 18; + + /** + * Command code for {@link MediaController2#prepareFromMediaId(String, Bundle)}. + */ + public static final int COMMAND_CODE_PREPARE_FROM_MEDIA_ID = 19; + + /** + * Command code for {@link MediaController2#prepareFromUri(Uri, Bundle)}. + */ + public static final int COMMAND_CODE_PREPARE_FROM_URI = 20; + + /** + * Command code for {@link MediaController2#prepareFromSearch(String, Bundle)}. + */ + public static final int COMMAND_CODE_PREPARE_FROM_SEARCH = 21; + + /** + * Command code for {@link MediaBrowser2} specific functions that allows navigation and search + * from the {@link MediaLibraryService2}. This would be ignored if a {@link MediaSession2}, + * not {@link android.media.MediaLibraryService2.MediaLibrarySession}, specify this. + * + * @see MediaBrowser2 + */ + public static final int COMMAND_CODE_BROWSER = 22; + + /** + * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}. + * <p> + * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command. + * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and + * {@link #getCustomCommand()} shouldn't be {@code null}. + */ + // TODO(jaewan): Move this into the updatable. + public static final class Command { + private final CommandProvider mProvider; + + public Command(@NonNull Context context, int commandCode) { + mProvider = ApiLoader.getProvider(context) + .createMediaSession2Command(this, commandCode, null, null); + } + + public Command(@NonNull Context context, @NonNull String action, @Nullable Bundle extra) { + if (action == null) { + throw new IllegalArgumentException("action shouldn't be null"); + } + mProvider = ApiLoader.getProvider(context) + .createMediaSession2Command(this, COMMAND_CODE_CUSTOM, action, extra); + } + + public int getCommandCode() { + return mProvider.getCommandCode_impl(); + } + + public @Nullable String getCustomCommand() { + return mProvider.getCustomCommand_impl(); + } + + public @Nullable Bundle getExtra() { + return mProvider.getExtra_impl(); + } + + /** + * @return a new Bundle instance from the Command + * @hide + */ + public Bundle toBundle() { + return mProvider.toBundle_impl(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Command)) { + return false; + } + return mProvider.equals_impl(((Command) obj).mProvider); + } + + @Override + public int hashCode() { + return mProvider.hashCode_impl(); + } + + /** + * @return a new Command instance from the Bundle + * @hide + */ + public static Command fromBundle(@NonNull Context context, Bundle command) { + return ApiLoader.getProvider(context).fromBundle_MediaSession2Command(context, command); + } + } + + /** + * Represent set of {@link Command}. + */ + public static class CommandGroup { + private final CommandGroupProvider mProvider; + + public CommandGroup(Context context) { + mProvider = ApiLoader.getProvider(context) + .createMediaSession2CommandGroup(context, this, null); + } + + public CommandGroup(Context context, CommandGroup others) { + mProvider = ApiLoader.getProvider(context) + .createMediaSession2CommandGroup(context, this, others); + } + + public void addCommand(Command command) { + mProvider.addCommand_impl(command); + } + + public void addAllPredefinedCommands() { + mProvider.addAllPredefinedCommands_impl(); + } + + public void removeCommand(Command command) { + mProvider.removeCommand_impl(command); + } + + public boolean hasCommand(Command command) { + return mProvider.hasCommand_impl(command); + } + + public boolean hasCommand(int code) { + return mProvider.hasCommand_impl(code); + } + + /** + * @hide + */ + @SystemApi + public CommandGroupProvider getProvider() { + return mProvider; + } + + /** + * @return new bundle from the CommandGroup + * @hide + */ + public Bundle toBundle() { + return mProvider.toBundle_impl(); + } + + /** + * @return new instance of CommandGroup from the bundle + * @hide + */ + public static @Nullable CommandGroup fromBundle(Context context, Bundle commands) { + return ApiLoader.getProvider(context) + .fromBundle_MediaSession2CommandGroup(context, commands); + } + } + + /** + * Callback to be called for all incoming commands from {@link MediaController2}s. + * <p> + * If it's not set, the session will accept all controllers and all incoming commands by + * default. + */ + // TODO(jaewan): Can we move this inside of the updatable for default implementation. + public static class SessionCallback { + private final Context mContext; + + public SessionCallback(Context context) { + mContext = context; + } + + /** + * Called when a controller is created for this session. Return allowed commands for + * controller. By default it allows all connection requests and commands. + * <p> + * You can reject the connection by return {@code null}. In that case, controller receives + * {@link MediaController2.ControllerCallback#onDisconnected()} and cannot be usable. + * + * @param controller controller information. + * @return allowed commands. Can be {@code null} to reject coonnection. + */ + // TODO(jaewan): Change return type. Once we do, null is for reject. + public @Nullable CommandGroup onConnect(@NonNull ControllerInfo controller) { + CommandGroup commands = new CommandGroup(mContext); + commands.addAllPredefinedCommands(); + return commands; + } + + /** + * Called when a controller is disconnected + * + * @param controller controller information + */ + public void onDisconnected(@NonNull ControllerInfo controller) { } + + /** + * Called when a controller sent a command that will be sent directly to the player. Return + * {@code false} here to reject the request and stop sending command to the player. + * + * @param controller controller information. + * @param command a command. This method will be called for every single command. + * @return {@code true} if you want to accept incoming command. {@code false} otherwise. + * @see #COMMAND_CODE_PLAYBACK_PLAY + * @see #COMMAND_CODE_PLAYBACK_PAUSE + * @see #COMMAND_CODE_PLAYBACK_STOP + * @see #COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM + * @see #COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM + * @see #COMMAND_CODE_PLAYBACK_PREPARE + * @see #COMMAND_CODE_PLAYBACK_FAST_FORWARD + * @see #COMMAND_CODE_PLAYBACK_REWIND + * @see #COMMAND_CODE_PLAYBACK_SEEK_TO + * @see #COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM + * @see #COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS + * @see #COMMAND_CODE_PLAYLIST_ADD + * @see #COMMAND_CODE_PLAYLIST_REMOVE + * @see #COMMAND_CODE_PLAYLIST_GET + * @see #COMMAND_CODE_SET_VOLUME + */ + public boolean onCommandRequest(@NonNull ControllerInfo controller, + @NonNull Command command) { + return true; + } + + /** + * Called when a controller set rating on the currently playing contents by + * {@link MediaController2#setRating(Rating2)}. + * + * @param controller controller information + * @param rating new rating from the controller + */ + public void onSetRating(@NonNull ControllerInfo controller, @NonNull Rating2 rating) { } + + /** + * Called when a controller sent a custom command through + * {@link MediaController2#sendCustomCommand(Command, Bundle, ResultReceiver)}. + * + * @param controller controller information + * @param customCommand custom command. + * @param args optional arguments + * @param cb optional result receiver + */ + public void onCustomCommand(@NonNull ControllerInfo controller, + @NonNull Command customCommand, @Nullable Bundle args, + @Nullable ResultReceiver cb) { } + + /** + * Called when a controller requested to play a specific mediaId through + * {@link MediaController2#playFromMediaId(String, Bundle)}. + * + * @param controller controller information + * @param mediaId media id + * @param extras optional extra bundle + * @see #COMMAND_CODE_PLAY_FROM_MEDIA_ID + */ + public void onPlayFromMediaId(@NonNull ControllerInfo controller, + @NonNull String mediaId, @Nullable Bundle extras) { } + + /** + * Called when a controller requested to begin playback from a search query through + * {@link MediaController2#playFromSearch(String, Bundle)} + * <p> + * An empty query indicates that the app may play any music. The implementation should + * attempt to make a smart choice about what to play. + * + * @param controller controller information + * @param query query string. Can be empty to indicate any suggested media + * @param extras optional extra bundle + * @see #COMMAND_CODE_PLAY_FROM_SEARCH + */ + public void onPlayFromSearch(@NonNull ControllerInfo controller, + @NonNull String query, @Nullable Bundle extras) { } + + /** + * Called when a controller requested to play a specific media item represented by a URI + * through {@link MediaController2#playFromUri(Uri, Bundle)} + * + * @param controller controller information + * @param uri uri + * @param extras optional extra bundle + * @see #COMMAND_CODE_PLAY_FROM_URI + */ + public void onPlayFromUri(@NonNull ControllerInfo controller, + @NonNull Uri uri, @Nullable Bundle extras) { } + + /** + * Called when a controller requested to prepare for playing a specific mediaId through + * {@link MediaController2#prepareFromMediaId(String, Bundle)}. + * <p> + * During the preparation, a session should not hold audio focus in order to allow other + * sessions play seamlessly. The state of playback should be updated to + * {@link PlaybackState#STATE_PAUSED} after the preparation is done. + * <p> + * The playback of the prepared content should start in the later calls of + * {@link MediaSession2#play()}. + * <p> + * Override {@link #onPlayFromMediaId} to handle requests for starting + * playback without preparation. + * + * @param controller controller information + * @param mediaId media id to prepare + * @param extras optional extra bundle + * @see #COMMAND_CODE_PREPARE_FROM_MEDIA_ID + */ + public void onPrepareFromMediaId(@NonNull ControllerInfo controller, + @NonNull String mediaId, @Nullable Bundle extras) { } + + /** + * Called when a controller requested to prepare playback from a search query through + * {@link MediaController2#prepareFromSearch(String, Bundle)}. + * <p> + * An empty query indicates that the app may prepare any music. The implementation should + * attempt to make a smart choice about what to play. + * <p> + * The state of playback should be updated to {@link PlaybackState#STATE_PAUSED} after the + * preparation is done. The playback of the prepared content should start in the later + * calls of {@link MediaSession2#play()}. + * <p> + * Override {@link #onPlayFromSearch} to handle requests for starting playback without + * preparation. + * + * @param controller controller information + * @param query query string. Can be empty to indicate any suggested media + * @param extras optional extra bundle + * @see #COMMAND_CODE_PREPARE_FROM_SEARCH + */ + public void onPrepareFromSearch(@NonNull ControllerInfo controller, + @NonNull String query, @Nullable Bundle extras) { } + + /** + * Called when a controller requested to prepare a specific media item represented by a URI + * through {@link MediaController2#prepareFromUri(Uri, Bundle)}. + * <p></p> + * During the preparation, a session should not hold audio focus in order to allow + * other sessions play seamlessly. The state of playback should be updated to + * {@link PlaybackState#STATE_PAUSED} after the preparation is done. + * <p> + * The playback of the prepared content should start in the later calls of + * {@link MediaSession2#play()}. + * <p> + * Override {@link #onPlayFromUri} to handle requests for starting playback without + * preparation. + * + * @param controller controller information + * @param uri uri + * @param extras optional extra bundle + * @see #COMMAND_CODE_PREPARE_FROM_URI + */ + public void onPrepareFromUri(@NonNull ControllerInfo controller, + @NonNull Uri uri, @Nullable Bundle extras) { } + }; + + /** + * Base builder class for MediaSession2 and its subclass. Any change in this class should be + * also applied to the subclasses {@link MediaSession2.Builder} and + * {@link MediaLibraryService2.MediaLibrarySessionBuilder}. + * <p> + * APIs here should be package private, but should have documentations for developers. + * Otherwise, javadoc will generate documentation with the generic types such as follows. + * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre> + * <p> + * This class is hidden to prevent from generating test stub, which fails with + * 'unexpected bound' because it tries to auto generate stub class as follows. + * <pre>abstract static class BuilderBase< + * T extends android.media.MediaSession2, + * U extends android.media.MediaSession2.BuilderBase< + * T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre> + * @hide + */ + static abstract class BuilderBase + <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> { + private final BuilderBaseProvider<T, C> mProvider; + + BuilderBase(ProviderCreator<BuilderBase<T, U, C>, BuilderBaseProvider<T, C>> creator) { + mProvider = creator.createProvider(this); + } + + /** + * Set volume provider to configure this session to use remote volume handling. + * This must be called to receive volume button events, otherwise the system + * will adjust the appropriate stream volume for this session's player. + * <p> + * Set {@code null} to reset. + * + * @param volumeProvider The provider that will handle volume changes. Can be {@code null}. + */ + U setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) { + mProvider.setVolumeProvider_impl(volumeProvider); + return (U) this; + } + + /** + * Set the style of rating used by this session. Apps trying to set the + * rating should use this style. Must be one of the following: + * <ul> + * <li>{@link Rating2#RATING_NONE}</li> + * <li>{@link Rating2#RATING_3_STARS}</li> + * <li>{@link Rating2#RATING_4_STARS}</li> + * <li>{@link Rating2#RATING_5_STARS}</li> + * <li>{@link Rating2#RATING_HEART}</li> + * <li>{@link Rating2#RATING_PERCENTAGE}</li> + * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li> + * </ul> + */ + U setRatingType(@Rating2.Style int type) { + mProvider.setRatingType_impl(type); + return (U) this; + } + + /** + * Set an intent for launching UI for this Session. This can be used as a + * quick link to an ongoing media screen. The intent should be for an + * activity that may be started using {@link Context#startActivity(Intent)}. + * + * @param pi The intent to launch to show UI for this session. + */ + U setSessionActivity(@Nullable PendingIntent pi) { + mProvider.setSessionActivity_impl(pi); + return (U) this; + } + + /** + * Set ID of the session. If it's not set, an empty string with used to create a session. + * <p> + * Use this if and only if your app supports multiple playback at the same time and also + * wants to provide external apps to have finer controls of them. + * + * @param id id of the session. Must be unique per package. + * @throws IllegalArgumentException if id is {@code null} + * @return + */ + U setId(@NonNull String id) { + mProvider.setId_impl(id); + return (U) this; + } + + /** + * Set callback for the session. + * + * @param executor callback executor + * @param callback session callback. + * @return + */ + U setSessionCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull C callback) { + mProvider.setSessionCallback_impl(executor, callback); + return (U) this; + } + + /** + * Build {@link MediaSession2}. + * + * @return a new session + * @throws IllegalStateException if the session with the same id is already exists for the + * package. + */ + T build() { + return mProvider.build_impl(); + } + } + + /** + * Builder for {@link MediaSession2}. + * <p> + * Any incoming event from the {@link MediaController2} will be handled on the thread + * that created session with the {@link Builder#build()}. + */ + // Override all methods just to show them with the type instead of generics in Javadoc. + // This workarounds javadoc issue described in the MediaSession2.BuilderBase. + public static final class Builder extends BuilderBase<MediaSession2, Builder, SessionCallback> { + public Builder(Context context, @NonNull MediaPlayerInterface player) { + super((instance) -> ApiLoader.getProvider(context).createMediaSession2Builder( + context, (Builder) instance, player)); + } + + @Override + public Builder setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) { + return super.setVolumeProvider(volumeProvider); + } + + @Override + public Builder setRatingType(@Rating2.Style int type) { + return super.setRatingType(type); + } + + @Override + public Builder setSessionActivity(@Nullable PendingIntent pi) { + return super.setSessionActivity(pi); + } + + @Override + public Builder setId(@NonNull String id) { + return super.setId(id); + } + + @Override + public Builder setSessionCallback(@NonNull Executor executor, + @Nullable SessionCallback callback) { + return super.setSessionCallback(executor, callback); + } + + @Override + public MediaSession2 build() { + return super.build(); + } + } + + /** + * Information of a controller. + */ + public static final class ControllerInfo { + private final ControllerInfoProvider mProvider; + + /** + * @hide + */ + // TODO(jaewan): SystemApi + // TODO(jaewan): Also accept componentName to check notificaiton listener. + public ControllerInfo(Context context, int uid, int pid, String packageName, + IInterface callback) { + mProvider = ApiLoader.getProvider(context) + .createMediaSession2ControllerInfo( + context, this, uid, pid, packageName, callback); + } + + /** + * @return package name of the controller + */ + public String getPackageName() { + return mProvider.getPackageName_impl(); + } + + /** + * @return uid of the controller + */ + public int getUid() { + return mProvider.getUid_impl(); + } + + /** + * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or + * has a enabled notification listener so can be trusted to accept connection and incoming + * command request. + * + * @return {@code true} if the controller is trusted. + */ + public boolean isTrusted() { + return mProvider.isTrusted_impl(); + } + + /** + * @hide + */ + @SystemApi + public ControllerInfoProvider getProvider() { + return mProvider; + } + + @Override + public int hashCode() { + return mProvider.hashCode_impl(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ControllerInfo)) { + return false; + } + ControllerInfo other = (ControllerInfo) obj; + return mProvider.equals_impl(other.mProvider); + } + + @Override + public String toString() { + // TODO(jaewan): Move this to updatable. + return "ControllerInfo {pkg=" + getPackageName() + ", uid=" + getUid() + ", trusted=" + + isTrusted() + "}"; + } + } + + /** + * Button for a {@link Command} that will be shown by the controller. + * <p> + * It's up to the controller's decision to respect or ignore this customization request. + */ + public static class CommandButton { + private final CommandButtonProvider mProvider; + + /** + * @hide + */ + @SystemApi + public CommandButton(CommandButtonProvider provider) { + mProvider = provider; + } + + /** + * Get command associated with this button. Can be {@code null} if the button isn't enabled + * and only providing placeholder. + * + * @return command or {@code null} + */ + public @Nullable Command getCommand() { + return mProvider.getCommand_impl(); + } + + /** + * Resource id of the button in this package. Can be {@code 0} if the command is predefined + * and custom icon isn't needed. + * + * @return resource id of the icon. Can be {@code 0}. + */ + public int getIconResId() { + return mProvider.getIconResId_impl(); + } + + /** + * Display name of the button. Can be {@code null} or empty if the command is predefined + * and custom name isn't needed. + * + * @return custom display name. Can be {@code null} or empty. + */ + public @Nullable String getDisplayName() { + return mProvider.getDisplayName_impl(); + } + + /** + * Extra information of the button. It's private information between session and controller. + * + * @return + */ + public @Nullable Bundle getExtra() { + return mProvider.getExtra_impl(); + } + + /** + * Return whether it's enabled + * + * @return {@code true} if enabled. {@code false} otherwise. + */ + public boolean isEnabled() { + return mProvider.isEnabled_impl(); + } + + /** + * @hide + */ + @SystemApi + public CommandButtonProvider getProvider() { + return mProvider; + } + + /** + * Builder for {@link CommandButton}. + */ + public static class Builder { + private final CommandButtonProvider.BuilderProvider mProvider; + + public Builder(@NonNull Context context) { + mProvider = ApiLoader.getProvider(context) + .createMediaSession2CommandButtonBuilder(context, this); + } + + public Builder setCommand(Command command) { + return mProvider.setCommand_impl(command); + } + + public Builder setIconResId(int resId) { + return mProvider.setIconResId_impl(resId); + } + + public Builder setDisplayName(String displayName) { + return mProvider.setDisplayName_impl(displayName); + } + + public Builder setEnabled(boolean enabled) { + return mProvider.setEnabled_impl(enabled); + } + + public Builder setExtra(Bundle extra) { + return mProvider.setExtra_impl(extra); + } + + public CommandButton build() { + return mProvider.build_impl(); + } + } + } + + /** + * Parameter for the playlist. + */ + public final static class PlaylistParams { + /** + * @hide + */ + @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL, + REPEAT_MODE_GROUP}) + @Retention(RetentionPolicy.SOURCE) + public @interface RepeatMode {} + + /** + * Playback will be stopped at the end of the playing media list. + */ + public static final int REPEAT_MODE_NONE = 0; + + /** + * Playback of the current playing media item will be repeated. + */ + public static final int REPEAT_MODE_ONE = 1; + + /** + * Playing media list will be repeated. + */ + public static final int REPEAT_MODE_ALL = 2; + + /** + * Playback of the playing media group will be repeated. + * A group is a logical block of media items which is specified in the section 5.7 of the + * Bluetooth AVRCP 1.6. + */ + public static final int REPEAT_MODE_GROUP = 3; + + /** + * @hide + */ + @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP}) + @Retention(RetentionPolicy.SOURCE) + public @interface ShuffleMode {} + + /** + * Media list will be played in order. + */ + public static final int SHUFFLE_MODE_NONE = 0; + + /** + * Media list will be played in shuffled order. + */ + public static final int SHUFFLE_MODE_ALL = 1; + + /** + * Media group will be played in shuffled order. + * A group is a logical block of media items which is specified in the section 5.7 of the + * Bluetooth AVRCP 1.6. + */ + public static final int SHUFFLE_MODE_GROUP = 2; + + + private final MediaSession2Provider.PlaylistParamsProvider mProvider; + + /** + * Instantiate {@link PlaylistParams} + * + * @param context context + * @param repeatMode repeat mode + * @param shuffleMode shuffle mode + * @param playlistMetadata metadata for the list + */ + public PlaylistParams(@NonNull Context context, @RepeatMode int repeatMode, + @ShuffleMode int shuffleMode, @Nullable MediaMetadata2 playlistMetadata) { + mProvider = ApiLoader.getProvider(context).createMediaSession2PlaylistParams( + context, this, repeatMode, shuffleMode, playlistMetadata); + } + + /** + * Create a new bundle for this object. + * + * @return + */ + public @NonNull Bundle toBundle() { + return mProvider.toBundle_impl(); + } + + /** + * Create a new playlist params from the bundle that was previously returned by + * {@link #toBundle}. + * + * @param context context + * @return a new playlist params. Can be {@code null} for error. + */ + public static @Nullable PlaylistParams fromBundle( + @NonNull Context context, @Nullable Bundle bundle) { + return ApiLoader.getProvider(context).fromBundle_PlaylistParams(context, bundle); + } + + /** + * Get repeat mode + * + * @return repeat mode + * @see #REPEAT_MODE_NONE, #REPEAT_MODE_ONE, #REPEAT_MODE_ALL, #REPEAT_MODE_GROUP + */ + public @RepeatMode int getRepeatMode() { + return mProvider.getRepeatMode_impl(); + } + + /** + * Get shuffle mode + * + * @return shuffle mode + * @see #SHUFFLE_MODE_NONE, #SHUFFLE_MODE_ALL, #SHUFFLE_MODE_GROUP + */ + public @ShuffleMode int getShuffleMode() { + return mProvider.getShuffleMode_impl(); + } + + /** + * Get metadata for the playlist + * + * @return metadata. Can be {@code null} + */ + public @Nullable MediaMetadata2 getPlaylistMetadata() { + return mProvider.getPlaylistMetadata_impl(); + } + } + + /** + * Constructor is hidden and apps can only instantiate indirectly through {@link Builder}. + * <p> + * This intended behavior and here's the reasons. + * 1. Prevent multiple sessions with the same tag in a media app. + * Whenever it happens only one session was properly setup and others were all dummies. + * Android framework couldn't find the right session to dispatch media key event. + * 2. Simplify session's lifecycle. + * {@link MediaSession} can be available after all of {@link MediaSession#setFlags(int)}, + * {@link MediaSession#setCallback(Callback)}, and + * {@link MediaSession#setActive(boolean)}. It was common for an app to omit one, so + * framework had to add heuristics to figure out if an app is + * @hide + */ + @SystemApi + public MediaSession2(MediaSession2Provider provider) { + super(); + mProvider = provider; + } + + /** + * @hide + */ + @SystemApi + public MediaSession2Provider getProvider() { + return mProvider; + } + + /** + * Set the underlying {@link MediaPlayerInterface} for this session to dispatch incoming event + * to. Events from the {@link MediaController2} will be sent directly to the underlying + * player on the {@link Handler} where the session is created on. + * <p> + * If the new player is successfully set, {@link PlaybackListener} + * will be called to tell the current playback state of the new player. + * <p> + * For the remote playback case which you want to handle volume by yourself, use + * {@link #setPlayer(MediaPlayerInterface, VolumeProvider2)}. + * + * @param player a {@link MediaPlayerInterface} that handles actual media playback in your app. + * @throws IllegalArgumentException if the player is {@code null}. + */ + public void setPlayer(@NonNull MediaPlayerInterface player) { + mProvider.setPlayer_impl(player); + } + + /** + * Set the underlying {@link MediaPlayerInterface} with the volume provider for remote playback. + * + * @param player a {@link MediaPlayerInterface} that handles actual media playback in your app. + * @param volumeProvider a volume provider + * @see #setPlayer(MediaPlayerInterface) + * @see Builder#setVolumeProvider(VolumeProvider2) + */ + public void setPlayer(@NonNull MediaPlayerInterface player, + @NonNull VolumeProvider2 volumeProvider) { + mProvider.setPlayer_impl(player, volumeProvider); + } + + @Override + public void close() { + mProvider.close_impl(); + } + + /** + * @return player + */ + public @Nullable + MediaPlayerInterface getPlayer() { + return mProvider.getPlayer_impl(); + } + + /** + * Returns the {@link SessionToken2} for creating {@link MediaController2}. + */ + public @NonNull + SessionToken2 getToken() { + return mProvider.getToken_impl(); + } + + public @NonNull List<ControllerInfo> getConnectedControllers() { + return mProvider.getConnectedControllers_impl(); + } + + /** + * Sets which type of audio focus will be requested during the playback, or configures playback + * to not request audio focus. Valid values for focus requests are + * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT}, + * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and + * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use + * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be + * requested when playback starts. You can for instance use this when playing a silent animation + * through this class, and you don't want to affect other audio applications playing in the + * background. + * + * @param focusGain the type of audio focus gain that will be requested, or + * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during + * playback. + */ + public void setAudioFocusRequest(int focusGain) { + mProvider.setAudioFocusRequest_impl(focusGain); + } + + /** + * Sets ordered list of {@link CommandButton} for controllers to build UI with it. + * <p> + * It's up to controller's decision how to represent the layout in its own UI. + * Here's the same way + * (layout[i] means a CommandButton at index i in the given list) + * For 5 icons row + * layout[3] layout[1] layout[0] layout[2] layout[4] + * For 3 icons row + * layout[1] layout[0] layout[2] + * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button) + * expanded row: layout[5] layout[6] layout[7] layout[8] layout[9] + * main row: layout[3] layout[1] layout[0] layout[2] layout[4] + * <p> + * This API can be called in the {@link SessionCallback#onConnect(ControllerInfo)}. + * + * @param controller controller to specify layout. + * @param layout oredered list of layout. + */ + public void setCustomLayout(@NonNull ControllerInfo controller, + @NonNull List<CommandButton> layout) { + mProvider.setCustomLayout_impl(controller, layout); + } + + /** + * Set the new allowed command group for the controller + * + * @param controller controller to change allowed commands + * @param commands new allowed commands + */ + public void setAllowedCommands(@NonNull ControllerInfo controller, + @NonNull CommandGroup commands) { + mProvider.setAllowedCommands_impl(controller, commands); + } + + /** + * Notify changes in metadata of previously set playlist. Controller will get the whole set of + * playlist again. + */ + public void notifyMetadataChanged() { + mProvider.notifyMetadataChanged_impl(); + } + + /** + * Send custom command to all connected controllers. + * + * @param command a command + * @param args optional argument + */ + public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args) { + mProvider.sendCustomCommand_impl(command, args); + } + + /** + * Send custom command to a specific controller. + * + * @param command a command + * @param args optional argument + * @param receiver result receiver for the session + */ + public void sendCustomCommand(@NonNull ControllerInfo controller, @NonNull Command command, + @Nullable Bundle args, @Nullable ResultReceiver receiver) { + // Equivalent to the MediaController.sendCustomCommand(Action action, ResultReceiver r); + mProvider.sendCustomCommand_impl(controller, command, args, receiver); + } + + /** + * Play playback + */ + public void play() { + mProvider.play_impl(); + } + + /** + * Pause playback + */ + public void pause() { + mProvider.pause_impl(); + } + + /** + * Stop playback + */ + public void stop() { + mProvider.stop_impl(); + } + + /** + * Rewind playback + */ + public void skipToPrevious() { + mProvider.skipToPrevious_impl(); + } + + /** + * Rewind playback + */ + public void skipToNext() { + mProvider.skipToNext_impl(); + } + + /** + * Request that the player prepare its playback. In other words, other sessions can continue + * to play during the preparation of this session. This method can be used to speed up the + * start of the playback. Once the preparation is done, the session will change its playback + * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to + * start playback. + */ + public void prepare() { + mProvider.prepare_impl(); + } + + /** + * Start fast forwarding. If playback is already fast forwarding this may increase the rate. + */ + public void fastForward() { + mProvider.fastForward_impl(); + } + + /** + * Start rewinding. If playback is already rewinding this may increase the rate. + */ + public void rewind() { + mProvider.rewind_impl(); + } + + /** + * Move to a new location in the media stream. + * + * @param pos Position to move to, in milliseconds. + */ + public void seekTo(long pos) { + mProvider.seekTo_impl(pos); + } + + /** + * Sets the index of current DataSourceDesc in the play list to be played. + * + * @param index the index of DataSourceDesc in the play list you want to play + * @throws IllegalArgumentException if the play list is null + * @throws NullPointerException if index is outside play list range + */ + public void setCurrentPlaylistItem(int index) { + mProvider.setCurrentPlaylistItem_impl(index); + } + + /** + * @hide + */ + public void skipForward() { + // To match with KEYCODE_MEDIA_SKIP_FORWARD + } + + /** + * @hide + */ + public void skipBackward() { + // To match with KEYCODE_MEDIA_SKIP_BACKWARD + } + + /** + * Sets a list of {@link MediaItem2} as the current play list. + * + * @param playlist A list of {@link MediaItem2} objects to set as a play list. + * @throws IllegalArgumentException if given {@param playlist} is null. + */ + public void setPlaylist(@NonNull List<MediaItem2> playlist) { + mProvider.setPlaylist_impl(playlist); + } + + /** + * Returns the playlist which is lastly set. + */ + public List<MediaItem2> getPlaylist() { + return mProvider.getPlaylist_impl(); + } + + /** + * Sets the {@link PlaylistParams} for the current play list. Repeat/shuffle mode and metadata + * for the list can be set by calling this method. + * + * @param params A {@link PlaylistParams} object to set. + * @throws IllegalArgumentException if given {@param param} is null. + */ + public void setPlaylistParams(PlaylistParams params) { + mProvider.setPlaylistParams_impl(params); + } + + /** + * Returns the {@link PlaylistParams} for the current play list. + * Returns {@code null} if not set. + */ + public PlaylistParams getPlaylistParams() { + return mProvider.getPlaylistParams_impl(); + } + + /* + * Add a {@link PlaybackListener} to listen changes in the underlying + * {@link MediaPlayerInterface}. Listener will be called immediately to tell the current value. + * <p> + * Added listeners will be also called when the underlying player is changed. + * + * @param executor the call listener + * @param listener the listener that will be run + * @throws IllegalArgumentException when either the listener or handler is {@code null}. + */ + public void addPlaybackListener(@NonNull @CallbackExecutor Executor executor, + @NonNull PlaybackListener listener) { + mProvider.addPlaybackListener_impl(executor, listener); + } + + /** + * Remove previously added {@link PlaybackListener}. + * + * @param listener the listener to be removed + * @throws IllegalArgumentException if the listener is {@code null}. + */ + public void removePlaybackListener(@NonNull PlaybackListener listener) { + mProvider.removePlaybackListener_impl(listener); + } + + /** + * Return the {@link PlaybackState2} from the player. + * + * @return playback state + */ + public PlaybackState2 getPlaybackState() { + return mProvider.getPlaybackState_impl(); + } +} diff --git a/media/java/android/media/MediaSessionService2.java b/media/java/android/media/MediaSessionService2.java new file mode 100644 index 000000000000..0b5dddf92af5 --- /dev/null +++ b/media/java/android/media/MediaSessionService2.java @@ -0,0 +1,235 @@ +/* + * Copyright 2018 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.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.media.MediaSession2.ControllerInfo; +import android.media.update.ApiLoader; +import android.media.update.MediaSessionService2Provider; +import android.media.update.MediaSessionService2Provider.MediaNotificationProvider; +import android.os.IBinder; + +/** + * Base class for media session services, which is the service version of the {@link MediaSession2}. + * <p> + * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants + * to keep media playback in the background. + * <p> + * Here's the benefits of using {@link MediaSessionService2} instead of + * {@link MediaSession2}. + * <ul> + * <li>Another app can know that your app supports {@link MediaSession2} even when your app + * isn't running. + * <li>Another app can start playback of your app even when your app isn't running. + * </ul> + * For example, user's voice command can start playback of your app even when it's not running. + * <p> + * To extend this class, adding followings directly to your {@code AndroidManifest.xml}. + * <pre> + * <service android:name="component_name_of_your_implementation" > + * <intent-filter> + * <action android:name="android.media.MediaSessionService2" /> + * </intent-filter> + * </service></pre> + * <p> + * A {@link MediaSessionService2} is another form of {@link MediaSession2}. IDs shouldn't + * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By + * default, an empty string will be used for ID of the service. If you want to specify an ID, + * declare metadata in the manifest as follows. + * <pre> + * <service android:name="component_name_of_your_implementation" > + * <intent-filter> + * <action android:name="android.media.MediaSessionService2" /> + * </intent-filter> + * <meta-data android:name="android.media.session" + * android:value="session_id"/> + * </service></pre> + * <p> + * It's recommended for an app to have a single {@link MediaSessionService2} declared in the + * manifest. Otherwise, your app might be shown twice in the list of the Auto/Wearable, or another + * app fails to pick the right session service when it wants to start the playback this app. + * <p> + * If there's conflicts with the session ID among the services, services wouldn't be available for + * any controllers. + * <p> + * Topic covered here: + * <ol> + * <li><a href="#ServiceLifecycle">Service Lifecycle</a> + * <li><a href="#Permissions">Permissions</a> + * </ol> + * <div class="special reference"> + * <a name="ServiceLifecycle"></a> + * <h3>Service Lifecycle</h3> + * <p> + * Session service is bounded service. When a {@link MediaController2} is created for the + * session service, the controller binds to the session service. {@link #onCreateSession(String)} + * may be called after the {@link #onCreate} if the service hasn't created yet. + * <p> + * After the binding, session's {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)} + * will be called to accept or reject connection request from a controller. If the connection is + * rejected, the controller will unbind. If it's accepted, the controller will be available to use + * and keep binding. + * <p> + * When playback is started for this session service, {@link #onUpdateNotification(PlaybackState2)} + * is called and service would become a foreground service. It's needed to keep playback after the + * controller is destroyed. The session service becomes background service when the playback is + * stopped. + * <a name="Permissions"></a> + * <h3>Permissions</h3> + * <p> + * Any app can bind to the session service with controller, but the controller can be used only if + * the session service accepted the connection request through + * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}. + * @hide + */ +public abstract class MediaSessionService2 extends Service { + private final MediaSessionService2Provider mProvider; + + /** + * This is the interface name that a service implementing a session service should say that it + * support -- that is, this is the action it uses for its intent filter. + */ + public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2"; + + /** + * Name under which a MediaSessionService2 component publishes information about itself. + * This meta-data must provide a string value for the ID. + */ + public static final String SERVICE_META_DATA = "android.media.session"; + + public MediaSessionService2() { + super(); + mProvider = createProvider(); + } + + MediaSessionService2Provider createProvider() { + return ApiLoader.getProvider(this).createMediaSessionService2(this); + } + + /** + * Default implementation for {@link MediaSessionService2} to initialize session service. + * <p> + * Override this method if you need your own initialization. Derived classes MUST call through + * to the super class's implementation of this method. + */ + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mProvider.onCreate_impl(); + } + + /** + * Called when another app requested to start this service to get {@link MediaSession2}. + * <p> + * Session service will accept or reject the connection with the + * {@link MediaSession2.SessionCallback} in the created session. + * <p> + * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the + * expected ID that you've specified through the AndroidManifest.xml. + * <p> + * This method will be called on the main thread. + * + * @param sessionId session id written in the AndroidManifest.xml. + * @return a new session + * @see MediaSession2.Builder + * @see #getSession() + */ + public @NonNull abstract MediaSession2 onCreateSession(String sessionId); + + /** + * Called when the playback state of this session is changed, and notification needs update. + * Override this method to show your own notification UI. + * <p> + * With the notification returned here, the service become foreground service when the playback + * is started. It becomes background service after the playback is stopped. + * + * @param state playback state + * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown. + */ + public MediaNotification onUpdateNotification(PlaybackState2 state) { + return mProvider.onUpdateNotification_impl(state); + } + + /** + * Get instance of the {@link MediaSession2} that you've previously created with the + * {@link #onCreateSession} for this service. + * + * @return created session + */ + public final MediaSession2 getSession() { + return mProvider.getSession_impl(); + } + + /** + * Default implementation for {@link MediaSessionService2} to handle incoming binding + * request. If the request is for getting the session, the intent will have action + * {@link #SERVICE_INTERFACE}. + * <p> + * Override this method if this service also needs to handle binder requests other than + * {@link #SERVICE_INTERFACE}. Derived classes MUST call through to the super class's + * implementation of this method. + * + * @param intent + * @return Binder + */ + @CallSuper + @Nullable + @Override + public IBinder onBind(Intent intent) { + return mProvider.onBind_impl(intent); + } + + /** + * Returned by {@link #onUpdateNotification(PlaybackState2)} for making session service + * foreground service to keep playback running in the background. It's highly recommended to + * show media style notification here. + */ + public static class MediaNotification { + private final MediaNotificationProvider mProvider; + + /** + * Default constructor + * + * @param context context + * @param notificationId notification id to be used for + * {@link android.app.NotificationManager#notify(int, Notification)}. + * @param notification a notification to make session service foreground service. Media + * style notification is recommended here. + */ + public MediaNotification(@NonNull Context context, + int notificationId, @NonNull Notification notification) { + mProvider = ApiLoader.getProvider(context) + .createMediaSessionService2MediaNotification( + context, this, notificationId, notification); + } + + public int getNotificationId() { + return mProvider.getNotificationId_impl(); + } + + public Notification getNotification() { + return mProvider.getNotification_impl(); + } + } +} diff --git a/media/java/android/media/MicrophoneInfo.java b/media/java/android/media/MicrophoneInfo.java new file mode 100644 index 000000000000..131e37bd6646 --- /dev/null +++ b/media/java/android/media/MicrophoneInfo.java @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2018 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.annotation.IntDef; +import android.util.Pair; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +/** + * Class providing information on a microphone. It indicates the location and orientation of the + * microphone on the device as well as useful information like frequency response and sensitivity. + * It can be used by applications implementing special pre processing effects like noise suppression + * of beam forming that need to know about precise microphone characteristics in order to adapt + * their algorithms. + */ +public final class MicrophoneInfo { + + /** + * A microphone that the location is unknown. + */ + public static final int LOCATION_UNKNOWN = 0; + + /** + * A microphone that locate on main body of the device. + */ + public static final int LOCATION_MAINBODY = 1; + + /** + * A microphone that locate on a movable main body of the device. + */ + public static final int LOCATION_MAINBODY_MOVABLE = 2; + + /** + * A microphone that locate on a peripheral. + */ + public static final int LOCATION_PERIPHERAL = 3; + + /** + * Unknown microphone directionality. + */ + public static final int DIRECTIONALITY_UNKNOWN = 0; + + /** + * Microphone directionality type: omni. + */ + public static final int DIRECTIONALITY_OMNI = 1; + + /** + * Microphone directionality type: bi-directional. + */ + public static final int DIRECTIONALITY_BI_DIRECTIONAL = 2; + + /** + * Microphone directionality type: cardioid. + */ + public static final int DIRECTIONALITY_CARDIOID = 3; + + /** + * Microphone directionality type: hyper cardioid. + */ + public static final int DIRECTIONALITY_HYPER_CARDIOID = 4; + + /** + * Microphone directionality type: super cardioid. + */ + public static final int DIRECTIONALITY_SUPER_CARDIOID = 5; + + /** + * The channel contains raw audio from this microphone. + */ + public static final int CHANNEL_MAPPING_DIRECT = 1; + + /** + * The channel contains processed audio from this microphone and possibly another microphone. + */ + public static final int CHANNEL_MAPPING_PROCESSED = 2; + + /** @hide */ + @IntDef(flag = true, prefix = { "LOCATION_" }, value = { + LOCATION_UNKNOWN, + LOCATION_MAINBODY, + LOCATION_MAINBODY_MOVABLE, + LOCATION_PERIPHERAL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface MicrophoneLocation {} + + /** @hide */ + @IntDef(flag = true, prefix = { "DIRECTIONALITY_" }, value = { + DIRECTIONALITY_UNKNOWN, + DIRECTIONALITY_OMNI, + DIRECTIONALITY_BI_DIRECTIONAL, + DIRECTIONALITY_CARDIOID, + DIRECTIONALITY_HYPER_CARDIOID, + DIRECTIONALITY_SUPER_CARDIOID, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface MicrophoneDirectionality {} + + private Coordinate3F mPosition; + private Coordinate3F mOrientation; + private String mDeviceId; + private String mAddress; + private List<Pair<Float, Float>> mFrequencyResponse; + private List<Pair<Integer, Integer>> mChannelMapping; + private float mMaxSpl; + private float mMinSpl; + private float mSensitivity; + private int mLocation; + private int mGroup; /* Usually 0 will be used for main body. */ + private int mIndexInTheGroup; + private int mPortId; /* mPortId will correspond to the id in AudioPort */ + private int mType; + private int mDirectionality; + + MicrophoneInfo(String deviceId, int type, String address, int location, + int group, int indexInTheGroup, Coordinate3F position, + Coordinate3F orientation, List<Pair<Float, Float>> frequencyResponse, + List<Pair<Integer, Integer>> channelMapping, float sensitivity, float maxSpl, + float minSpl, int directionality) { + mDeviceId = deviceId; + mType = type; + mAddress = address; + mLocation = location; + mGroup = group; + mIndexInTheGroup = indexInTheGroup; + mPosition = position; + mOrientation = orientation; + mFrequencyResponse = frequencyResponse; + mChannelMapping = channelMapping; + mSensitivity = sensitivity; + mMaxSpl = maxSpl; + mMinSpl = minSpl; + mDirectionality = directionality; + } + + /** + * Returns alphanumeric code that uniquely identifies the device. + * + * @return the description of the microphone + */ + public String getDescription() { + return mDeviceId; + } + + /** + * Returns The system unique device ID that corresponds to the id + * returned by {@link AudioDeviceInfo#getId()}. + * + * @return the microphone's id + */ + public int getId() { + return mPortId; + } + + /** + * @hide + * Returns the internal device type (e.g AudioSystem.DEVICE_IN_BUILTIN_MIC). + * The internal device type could be used when getting microphone's port id + * by matching type and address. + * + * @return the internal device type + */ + public int getInternalDeviceType() { + return mType; + } + + /** + * Returns the device type identifier of the microphone (e.g AudioDeviceInfo.TYPE_BUILTIN_MIC). + * + * @return the device type of the microphone + */ + public int getType() { + return AudioDeviceInfo.convertInternalDeviceToDeviceType(mType); + } + + /** + * @hide + * Returns The "address" string of the microphone that corresponds to the + * address returned by {@link AudioDeviceInfo#getAddress()} + * @return the address of the microphone + */ + public String getAddress() { + return mAddress; + } + + /** + * Returns the location of the microphone. The return value is + * one of {@link #LOCATION_UNKNOWN}, {@link #LOCATION_MAINBODY}, + * {@link #LOCATION_MAINBODY_MOVABLE}, or {@link #LOCATION_PERIPHERAL}. + * + * @return the location of the microphone + */ + public @MicrophoneLocation int getLocation() { + return mLocation; + } + + /** + * Returns A device group id that can be used to group together microphones on the same + * peripheral, attachments or logical groups. Main body is usually group 0. + * + * @return the group of the microphone + */ + public int getGroup() { + return mGroup; + } + + /** + * Returns unique index for device within its group. + * + * @return the microphone's index in its group + */ + public int getIndexInTheGroup() { + return mIndexInTheGroup; + } + + /** + * Returns A {@link Coordinate3F} object that represents the geometric location of microphone + * in meters, from botton-left-back corner of appliance. X-axis, Y-axis and Z-axis show + * as the x, y, z values. + * + * @return the geometric location of the microphone + */ + public Coordinate3F getPosition() { + return mPosition; + } + + /** + * Returns A {@link Coordinate3F} object that represents the orientation of microphone. + * X-axis, Y-axis and Z-axis show as the x, y, z value. The orientation will be normalized + * such as sqrt(x^2 + y^2 + z^2) equals 1. + * + * @return the orientation of the microphone + */ + public Coordinate3F getOrientation() { + return mOrientation; + } + + /** + * Returns a {@link android.util.Pair} list of frequency responses. + * For every {@link android.util.Pair} in the list, the first value represents frequency in Hz, + * and the second value represents response in dB. + * + * @return the frequency response of the microphone + */ + public List<Pair<Float, Float>> getFrequencyResponse() { + return mFrequencyResponse; + } + + /** + * Returns a {@link android.util.Pair} list for channel mapping, which indicating how this + * microphone is used by each channels or a capture stream. For each {@link android.util.Pair}, + * the first value is channel index, the second value is channel mapping type, which could be + * either {@link #CHANNEL_MAPPING_DIRECT} or {@link #CHANNEL_MAPPING_PROCESSED}. + * If a channel has contributions from more than one microphone, it is likely the HAL + * did some extra processing to combine the sources, but this is to be inferred by the user. + * Empty list when the MicrophoneInfo is returned by AudioManager.getMicrophones(). + * At least one entry when the MicrophoneInfo is returned by AudioRecord.getActiveMicrophones(). + * + * @return a {@link android.util.Pair} list for channel mapping + */ + public List<Pair<Integer, Integer>> getChannelMapping() { + return mChannelMapping; + } + + /** + * Returns the level in dBFS produced by a 1000Hz tone at 94 dB SPL. + * + * @return the sensitivity of the microphone + */ + public float getSensitivity() { + return mSensitivity; + } + + /** + * Returns the level in dB of the maximum SPL supported by the device at 1000Hz. + * + * @return the maximum level in dB + */ + public float getMaxSpl() { + return mMaxSpl; + } + + /** + * Returns the level in dB of the minimum SPL that can be registered by the device at 1000Hz. + * + * @return the minimum level in dB + */ + public float getMinSpl() { + return mMinSpl; + } + + /** + * Returns the directionality of microphone. The return value is one of + * {@link #DIRECTIONALITY_UNKNOWN}, {@link #DIRECTIONALITY_OMNI}, + * {@link #DIRECTIONALITY_BI_DIRECTIONAL}, {@link #DIRECTIONALITY_CARDIOID}, + * {@link #DIRECTIONALITY_HYPER_CARDIOID}, or {@link #DIRECTIONALITY_SUPER_CARDIOID}. + * + * @return the directionality of microphone + */ + public @MicrophoneDirectionality int getDirectionality() { + return mDirectionality; + } + + /** + * Set the port id for the device. + * @hide + */ + public void setId(int portId) { + mPortId = portId; + } + + /* A class containing three float value to represent a 3D coordinate */ + public class Coordinate3F { + public final float x; + public final float y; + public final float z; + + Coordinate3F(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + } +} diff --git a/media/java/android/media/NativeRoutingEventHandlerDelegate.java b/media/java/android/media/NativeRoutingEventHandlerDelegate.java new file mode 100644 index 000000000000..9a6baf17e860 --- /dev/null +++ b/media/java/android/media/NativeRoutingEventHandlerDelegate.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 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.os.Handler; + +/** + * Helper class {@link AudioTrack}, {@link AudioRecord}, {@link MediaPlayer} and {@link MediaRecorder} + * to handle the forwarding of native events to the appropriate listener + * (potentially) handled in a different thread. + * @hide + */ +class NativeRoutingEventHandlerDelegate { + private AudioRouting mAudioRouting; + private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener; + private Handler mHandler; + + NativeRoutingEventHandlerDelegate(final AudioRouting audioRouting, + final AudioRouting.OnRoutingChangedListener listener, Handler handler) { + mAudioRouting = audioRouting; + mOnRoutingChangedListener = listener; + mHandler = handler; + } + + void notifyClient() { + if (mHandler != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mOnRoutingChangedListener != null) { + mOnRoutingChangedListener.onRoutingChanged(mAudioRouting); + } + } + }); + } + } +} diff --git a/media/java/android/media/PlaybackState2.java b/media/java/android/media/PlaybackState2.java new file mode 100644 index 000000000000..627974a87137 --- /dev/null +++ b/media/java/android/media/PlaybackState2.java @@ -0,0 +1,230 @@ +/* + * Copyright 2018 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.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.update.ApiLoader; +import android.media.update.PlaybackState2Provider; +import android.os.Bundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Playback state for a {@link MediaPlayerInterface}, to be shared between {@link MediaSession2} and + * {@link MediaController2}. This includes a playback state {@link #STATE_PLAYING}, + * the current playback position and extra. + * @hide + */ +public final class PlaybackState2 { + // Similar to the PlaybackState2 with following changes + // - Not implement Parcelable and added from/toBundle() + // - Removed playback state that doesn't match with the MediaPlayer2 + // Full list should be finalized when the MediaPlayer2 has getter for the playback state. + // Here's table for the MP2 state and PlaybackState2.State. + // +----------------------------------------+----------------------------------------+ + // | MediaPlayer2 state | Matching PlaybackState2.State | + // | (Names are from MP2' Javadoc) | | + // +----------------------------------------+----------------------------------------+ + // | Idle: Just finished creating MP2 | STATE_NONE | + // | or reset() is called | | + // +----------------------------------------+----------------------------------------+ + // | Initialized: setDataSource/Playlist | N/A (Session/Controller don't | + // | | differentiate with Prepared) | + // +----------------------------------------+----------------------------------------+ + // | Prepared: Prepared after initialized | STATE_PAUSED | + // +----------------------------------------+----------------------------------------+ + // | Started: Started playback | STATE_PLAYING | + // +----------------------------------------+----------------------------------------+ + // | Paused: Paused playback | STATE_PAUSED | + // +----------------------------------------+----------------------------------------+ + // | PlaybackCompleted: Playback is done | STATE_PAUSED | + // +----------------------------------------+----------------------------------------+ + // | Stopped: MP2.stop() is called. | STATE_STOPPED | + // | prepare() is needed to play again | | + // | (Seemingly the same as initialized | | + // | because cannot set data source | | + // | after this) | | + // +----------------------------------------+----------------------------------------+ + // | Error: an API is called in a state | STATE_ERROR | + // | that the API isn't supported | | + // +----------------------------------------+----------------------------------------+ + // | End: MP2.close() is called to release | N/A (MediaSession will be gone) | + // | MP2. Cannot be reused anymore | | + // +----------------------------------------+----------------------------------------+ + // | Started, but | STATE_BUFFERING | + // | EventCallback.onBufferingUpdate() | | + // +----------------------------------------+----------------------------------------+ + // - Removed actions and custom actions. + // - Repeat mode / shuffle mode is now in the PlaylistParams + // TODO(jaewan): Replace states from MediaPlayer2 + /** + * @hide + */ + @IntDef({STATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_BUFFERING, STATE_ERROR}) + @Retention(RetentionPolicy.SOURCE) + public @interface State {} + + /** + * This is the default playback state and indicates that no media has been + * added yet, or the performer has been reset and has no content to play. + */ + public final static int STATE_NONE = 0; + + /** + * State indicating this item is currently stopped. + */ + public final static int STATE_STOPPED = 1; + + /** + * State indicating this item is currently paused. + */ + public final static int STATE_PAUSED = 2; + + /** + * State indicating this item is currently playing. + */ + public final static int STATE_PLAYING = 3; + + /** + * State indicating this item is currently buffering and will begin playing + * when enough data has buffered. + */ + public final static int STATE_BUFFERING = 4; + + /** + * State indicating this item is currently in an error state. The error + * message should also be set when entering this state. + */ + public final static int STATE_ERROR = 5; + + /** + * Use this value for the position to indicate the position is not known. + */ + public final static long PLAYBACK_POSITION_UNKNOWN = -1; + + private final PlaybackState2Provider mProvider; + + // TODO(jaewan): Better error handling? + // E.g. media item at #2 has issue, but continue playing #3 + // login error. fire intent xxx to login + public PlaybackState2(@NonNull Context context, int state, long position, long updateTime, + float speed, long bufferedPosition, long activeItemId, CharSequence error) { + mProvider = ApiLoader.getProvider(context).createPlaybackState2(context, this, state, + position, updateTime, speed, bufferedPosition, activeItemId, error); + } + + @Override + public String toString() { + return mProvider.toString_impl(); + } + + /** + * Get the current state of playback. One of the following: + * <ul> + * <li> {@link PlaybackState2#STATE_NONE}</li> + * <li> {@link PlaybackState2#STATE_STOPPED}</li> + * <li> {@link PlaybackState2#STATE_PREPARED}</li> + * <li> {@link PlaybackState2#STATE_PAUSED}</li> + * <li> {@link PlaybackState2#STATE_PLAYING}</li> + * <li> {@link PlaybackState2#STATE_FINISH}</li> + * <li> {@link PlaybackState2#STATE_BUFFERING}</li> + * <li> {@link PlaybackState2#STATE_ERROR}</li> + * </ul> + */ + @State + public int getState() { + return mProvider.getState_impl(); + } + + /** + * Get the current playback position in ms. + */ + public long getPosition() { + return mProvider.getPosition_impl(); + } + + /** + * Get the current buffered position in ms. This is the farthest playback + * point that can be reached from the current position using only buffered + * content. + */ + public long getBufferedPosition() { + return mProvider.getBufferedPosition_impl(); + } + + /** + * Get the current playback speed as a multiple of normal playback. This + * should be negative when rewinding. A value of 1 means normal playback and + * 0 means paused. + * + * @return The current speed of playback. + */ + public float getPlaybackSpeed() { + return mProvider.getPlaybackSpeed_impl(); + } + + /** + * Get a user readable error message. This should be set when the state is + * {@link PlaybackState2#STATE_ERROR}. + */ + public CharSequence getErrorMessage() { + return mProvider.getErrorMessage_impl(); + } + + /** + * Get the elapsed real time at which position was last updated. If the + * position has never been set this will return 0; + * + * @return The last time the position was updated. + */ + public long getLastPositionUpdateTime() { + return mProvider.getLastPositionUpdateTime_impl(); + } + + /** + * Get the id of the currently active item in the playlist. + * + * @return The id of the currently active item in the queue + */ + public long getCurrentPlaylistItemIndex() { + return mProvider.getCurrentPlaylistItemIndex_impl(); + } + + /** + * Returns this object as a bundle to share between processes. + */ + public @NonNull Bundle toBundle() { + return mProvider.toBundle_impl(); + } + + /** + * Creates an instance from a bundle which is previously created by {@link #toBundle()}. + * + * @param context context + * @param bundle A bundle created by {@link #toBundle()}. + * @return A new {@link PlaybackState2} instance. Returns {@code null} if the given + * {@param bundle} is null, or if the {@param bundle} has no playback state parameters. + */ + public @Nullable static PlaybackState2 fromBundle(@NonNull Context context, + @Nullable Bundle bundle) { + return ApiLoader.getProvider(context).fromBundle_PlaybackState2(context, bundle); + } +}
\ No newline at end of file diff --git a/media/java/android/media/Rating2.java b/media/java/android/media/Rating2.java new file mode 100644 index 000000000000..4f77ecd58149 --- /dev/null +++ b/media/java/android/media/Rating2.java @@ -0,0 +1,273 @@ +/* + * Copyright 2018 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.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.IntDef; +import android.annotation.SystemApi; +import android.content.Context; +import android.media.update.ApiLoader; +import android.media.update.Rating2Provider; +import android.os.Bundle; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A class to encapsulate rating information used as content metadata. + * A rating is defined by its rating style (see {@link #RATING_HEART}, + * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, + * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may + * be defined as "unrated"), both of which are defined when the rating instance is constructed + * through one of the factory methods. + * @hide + */ +public final class Rating2 { + // Mostly same as the android.media.Rating, but it's no longer implements Parcelable for + // updatable support. + + /** + * @hide + */ + @IntDef({RATING_NONE, RATING_HEART, RATING_THUMB_UP_DOWN, RATING_3_STARS, RATING_4_STARS, + RATING_5_STARS, RATING_PERCENTAGE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Style {} + + /** + * @hide + */ + @IntDef({RATING_3_STARS, RATING_4_STARS, RATING_5_STARS}) + @Retention(RetentionPolicy.SOURCE) + public @interface StarStyle {} + + /** + * Indicates a rating style is not supported. A Rating2 will never have this + * type, but can be used by other classes to indicate they do not support + * Rating2. + */ + public final static int RATING_NONE = 0; + + /** + * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to + * indicate the content referred to is a favorite (or not). + */ + public final static int RATING_HEART = 1; + + /** + * A rating style for "thumb up" vs "thumb down". + */ + public final static int RATING_THUMB_UP_DOWN = 2; + + /** + * A rating style with 0 to 3 stars. + */ + public final static int RATING_3_STARS = 3; + + /** + * A rating style with 0 to 4 stars. + */ + public final static int RATING_4_STARS = 4; + + /** + * A rating style with 0 to 5 stars. + */ + public final static int RATING_5_STARS = 5; + + /** + * A rating style expressed as a percentage. + */ + public final static int RATING_PERCENTAGE = 6; + + private final Rating2Provider mProvider; + + /** + * @hide + */ + @SystemApi + public Rating2(@NonNull Rating2Provider provider) { + mProvider = provider; + } + + @Override + public String toString() { + return mProvider.toString_impl(); + } + + /** + * @hide + */ + @SystemApi + public Rating2Provider getProvider() { + return mProvider; + } + + @Override + public boolean equals(Object obj) { + return mProvider.equals_impl(obj); + } + + @Override + public int hashCode() { + return mProvider.hashCode_impl(); + } + + /** + * Create an instance from bundle object, previoulsy created by {@link #toBundle()} + * + * @param context context + * @param bundle bundle + * @return new Rating2 instance or {@code null} for error + */ + public static Rating2 fromBundle(@NonNull Context context, @Nullable Bundle bundle) { + return ApiLoader.getProvider(context).fromBundle_Rating2(context, bundle); + } + + /** + * Return bundle for this object to share across the process. + * @return bundle of this object + */ + public Bundle toBundle() { + return mProvider.toBundle_impl(); + } + + /** + * Return a Rating2 instance with no rating. + * Create and return a new Rating2 instance with no rating known for the given + * rating style. + * @param context context + * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, + * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, + * or {@link #RATING_PERCENTAGE}. + * @return null if an invalid rating style is passed, a new Rating2 instance otherwise. + */ + public static @Nullable Rating2 newUnratedRating(@NonNull Context context, @Style int ratingStyle) { + return ApiLoader.getProvider(context).newUnratedRating_Rating2(context, ratingStyle); + } + + /** + * Return a Rating2 instance with a heart-based rating. + * Create and return a new Rating2 instance with a rating style of {@link #RATING_HEART}, + * and a heart-based rating. + * @param context context + * @param hasHeart true for a "heart selected" rating, false for "heart unselected". + * @return a new Rating2 instance. + */ + public static @Nullable Rating2 newHeartRating(@NonNull Context context, boolean hasHeart) { + return ApiLoader.getProvider(context).newHeartRating_Rating2(context, hasHeart); + } + + /** + * Return a Rating2 instance with a thumb-based rating. + * Create and return a new Rating2 instance with a {@link #RATING_THUMB_UP_DOWN} + * rating style, and a "thumb up" or "thumb down" rating. + * @param context context + * @param thumbIsUp true for a "thumb up" rating, false for "thumb down". + * @return a new Rating2 instance. + */ + public static @Nullable Rating2 newThumbRating(@NonNull Context context, boolean thumbIsUp) { + return ApiLoader.getProvider(context).newThumbRating_Rating2(context, thumbIsUp); + } + + /** + * Return a Rating2 instance with a star-based rating. + * Create and return a new Rating2 instance with one of the star-base rating styles + * and the given integer or fractional number of stars. Non integer values can for instance + * be used to represent an average rating value, which might not be an integer number of stars. + * @param context context + * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, + * {@link #RATING_5_STARS}. + * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to + * the rating style. + * @return null if the rating style is invalid, or the rating is out of range, + * a new Rating2 instance otherwise. + */ + public static @Nullable Rating2 newStarRating(@NonNull Context context, + @StarStyle int starRatingStyle, float starRating) { + return ApiLoader.getProvider(context).newStarRating_Rating2( + context, starRatingStyle, starRating); + } + + /** + * Return a Rating2 instance with a percentage-based rating. + * Create and return a new Rating2 instance with a {@link #RATING_PERCENTAGE} + * rating style, and a rating of the given percentage. + * @param context context + * @param percent the value of the rating + * @return null if the rating is out of range, a new Rating2 instance otherwise. + */ + public static @Nullable Rating2 newPercentageRating(@NonNull Context context, float percent) { + return ApiLoader.getProvider(context).newPercentageRating_Rating2(context, percent); + } + + /** + * Return whether there is a rating value available. + * @return true if the instance was not created with {@link #newUnratedRating(Context, int)}. + */ + public boolean isRated() { + return mProvider.isRated_impl(); + } + + /** + * Return the rating style. + * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, + * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, + * or {@link #RATING_PERCENTAGE}. + */ + @Style + public int getRatingStyle() { + return mProvider.getRatingStyle_impl(); + } + + /** + * Return whether the rating is "heart selected". + * @return true if the rating is "heart selected", false if the rating is "heart unselected", + * if the rating style is not {@link #RATING_HEART} or if it is unrated. + */ + public boolean hasHeart() { + return mProvider.hasHeart_impl(); + } + + /** + * Return whether the rating is "thumb up". + * @return true if the rating is "thumb up", false if the rating is "thumb down", + * if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated. + */ + public boolean isThumbUp() { + return mProvider.isThumbUp_impl(); + } + + /** + * Return the star-based rating value. + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is + * not star-based, or if it is unrated. + */ + public float getStarRating() { + return mProvider.getStarRating_impl(); + } + + /** + * Return the percentage-based rating value. + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is + * not percentage-based, or if it is unrated. + */ + public float getPercentRating() { + return mProvider.getPercentRating_impl(); + } +} diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 3eb9d529b756..fefa1ede849e 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -28,11 +28,13 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; import android.database.Cursor; import android.media.MediaScannerConnection.MediaScannerConnectionClient; import android.net.Uri; import android.os.Environment; +import android.os.FileUtils; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.Process; @@ -47,22 +49,17 @@ import android.util.Log; import com.android.internal.database.SortCursor; -import libcore.io.Streams; - import java.io.Closeable; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; -import static android.content.ContentProvider.maybeAddUserId; -import static android.content.pm.PackageManager.NameNotFoundException; - /** * RingtoneManager provides access to ringtones, notification, and other types * of sounds. It manages querying the different media providers and combines the @@ -855,7 +852,7 @@ public class RingtoneManager { final Uri cacheUri = getCacheForType(type, context.getUserId()); try (InputStream in = openRingtone(context, ringtoneUri); OutputStream out = resolver.openOutputStream(cacheUri)) { - Streams.copy(in, out); + FileUtils.copy(in, out); } catch (IOException e) { Log.w(TAG, "Failed to cache ringtone: " + e); } @@ -960,7 +957,7 @@ public class RingtoneManager { // Copy contents to external ringtone storage. Throws IOException if the copy fails. try (final InputStream input = mContext.getContentResolver().openInputStream(fileUri); final OutputStream output = new FileOutputStream(outFile)) { - Streams.copy(input, output); + FileUtils.copy(input, output); } // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}. diff --git a/media/java/android/media/SessionPlayer2.java b/media/java/android/media/SessionPlayer2.java new file mode 100644 index 000000000000..60acf1683bc3 --- /dev/null +++ b/media/java/android/media/SessionPlayer2.java @@ -0,0 +1,152 @@ +/* + * Copyright 2018 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.annotation.SystemApi; +import android.content.Context; +import android.media.MediaSession2.PlaylistParams; +import android.media.update.ApiLoader; +import android.media.update.SessionPlayer2Provider; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Implementation of the {@link MediaPlayerInterface} which is backed by the {@link MediaPlayer2} + * @hide + */ +public class SessionPlayer2 implements MediaPlayerInterface { + private final SessionPlayer2Provider mProvider; + + public SessionPlayer2(Context context) { + mProvider = ApiLoader.getProvider(context).createSessionPlayer2(context, this); + } + + @Override + public void play() { + mProvider.play_impl(); + } + + @Override + public void prepare() { + mProvider.prepare_impl(); + } + + @Override + public void pause() { + mProvider.pause_impl(); + } + + @Override + public void stop() { + mProvider.stop_impl(); + } + + @Override + public void skipToPrevious() { + mProvider.skipToPrevious_impl(); + } + + @Override + public void skipToNext() { + mProvider.skipToNext_impl(); + } + + @Override + public void seekTo(long pos) { + mProvider.seekTo_impl(pos); + } + + @Override + public void fastForward() { + mProvider.fastForward_impl(); + } + + @Override + public void rewind() { + mProvider.rewind_impl(); + } + + @Override + public PlaybackState2 getPlaybackState() { + return mProvider.getPlaybackState_impl(); + } + + @Override + public void setAudioAttributes(AudioAttributes attributes) { + mProvider.setAudioAttributes_impl(attributes); + } + + @Override + public AudioAttributes getAudioAttributes() { + return mProvider.getAudioAttributes_impl(); + } + + @Override + public void setPlaylist(List<MediaItem2> playlist) { + mProvider.setPlaylist_impl(playlist); + } + + @Override + public List<MediaItem2> getPlaylist() { + return mProvider.getPlaylist_impl(); + } + + @Override + public void setCurrentPlaylistItem(int index) { + mProvider.setCurrentPlaylistItem_impl(index); + } + + @Override + public void setPlaylistParams(PlaylistParams params) { + mProvider.setPlaylistParams_impl(params); + } + + @Override + public void addPlaylistItem(int index, MediaItem2 item) { + mProvider.addPlaylistItem_impl(index, item); + } + + @Override + public void removePlaylistItem(MediaItem2 item) { + mProvider.removePlaylistItem_impl(item); + } + + @Override + public PlaylistParams getPlaylistParams() { + return mProvider.getPlaylistParams_impl(); + } + + @Override + public void addPlaybackListener(Executor executor, PlaybackListener listener) { + mProvider.addPlaybackListener_impl(executor, listener); + } + + @Override + public void removePlaybackListener(PlaybackListener listener) { + mProvider.removePlaybackListener_impl(listener); + } + + public MediaPlayer2 getPlayer() { + return mProvider.getPlayer_impl(); + } + + @SystemApi + public SessionPlayer2Provider getProvider() { + return mProvider; + } +} diff --git a/media/java/android/media/SessionToken2.java b/media/java/android/media/SessionToken2.java new file mode 100644 index 000000000000..2c2090ceb785 --- /dev/null +++ b/media/java/android/media/SessionToken2.java @@ -0,0 +1,160 @@ +/* + * Copyright 2018 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.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; +import android.media.session.MediaSessionManager; +import android.media.update.ApiLoader; +import android.media.update.SessionToken2Provider; +import android.os.Bundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}. + * If it's representing a session service, it may not be ongoing. + * <p> + * This may be passed to apps by the session owner to allow them to create a + * {@link MediaController2} to communicate with the session. + * <p> + * It can be also obtained by {@link MediaSessionManager}. + * @hide + */ +public final class SessionToken2 { + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE}) + public @interface TokenType { + } + + public static final int TYPE_SESSION = 0; + public static final int TYPE_SESSION_SERVICE = 1; + public static final int TYPE_LIBRARY_SERVICE = 2; + + private final SessionToken2Provider mProvider; + + // From the return value of android.os.Process.getUidForName(String) when error + private static final int UID_UNKNOWN = -1; + + /** + * Constructor for the token. You can only create token for session service or library service + * to use by {@link MediaController2} or {@link MediaBrowser2}. + * + * @param context context + * @param packageName package name + * @param serviceName name of service. Can be {@code null} if it's not an service. + */ + public SessionToken2(@NonNull Context context, @NonNull String packageName, + @NonNull String serviceName) { + this(context, packageName, serviceName, UID_UNKNOWN); + } + + /** + * Constructor for the token. You can only create token for session service or library service + * to use by {@link MediaController2} or {@link MediaBrowser2}. + * + * @param context context + * @param packageName package name + * @param serviceName name of service. Can be {@code null} if it's not an service. + * @param uid uid of the app. + * @hide + */ + public SessionToken2(@NonNull Context context, @NonNull String packageName, + @NonNull String serviceName, int uid) { + mProvider = ApiLoader.getProvider(context).createSessionToken2( + context, this, packageName, serviceName, uid); + } + + /** + * Constructor for the token. + * @hide + */ + @SystemApi + public SessionToken2(@NonNull SessionToken2Provider provider) { + mProvider = provider; + } + + @Override + public int hashCode() { + return mProvider.hashCode_impl(); + } + + @Override + public boolean equals(Object obj) { + return mProvider.equals_impl(obj); + } + + @Override + public String toString() { + return mProvider.toString_impl(); + } + + @SystemApi + public SessionToken2Provider getProvider() { + return mProvider; + } + + /** + * @return uid of the session + */ + public int getUid() { + return mProvider.getUid_impl(); + } + + /** + * @return package name + */ + public String getPackageName() { + return mProvider.getPackageName_impl(); + } + + /** + * @return id + */ + public String getId() { + return mProvider.getId_imp(); + } + + /** + * @return type of the token + * @see #TYPE_SESSION + * @see #TYPE_SESSION_SERVICE + */ + public @TokenType int getType() { + return mProvider.getType_impl(); + } + + /** + * Create a token from the bundle, exported by {@link #toBundle()}. + * @param bundle + * @return + */ + public static SessionToken2 fromBundle(@NonNull Context context, @NonNull Bundle bundle) { + return ApiLoader.getProvider(context).SessionToken2_fromBundle(context, bundle); + } + + /** + * Create a {@link Bundle} from this token to share it across processes. + * @return Bundle + */ + public Bundle toBundle() { + return mProvider.toBundle_impl(); + } +} diff --git a/media/java/android/media/VolumePolicy.java b/media/java/android/media/VolumePolicy.java index bbcce82f2e2d..bd6667faff31 100644 --- a/media/java/android/media/VolumePolicy.java +++ b/media/java/android/media/VolumePolicy.java @@ -23,7 +23,7 @@ import java.util.Objects; /** @hide */ public final class VolumePolicy implements Parcelable { - public static final VolumePolicy DEFAULT = new VolumePolicy(false, false, true, 400); + public static final VolumePolicy DEFAULT = new VolumePolicy(false, false, false, 400); /** * Accessibility volume policy where the STREAM_MUSIC volume (i.e. media volume) affects diff --git a/media/java/android/media/VolumeProvider2.java b/media/java/android/media/VolumeProvider2.java new file mode 100644 index 000000000000..53ba4663aaf6 --- /dev/null +++ b/media/java/android/media/VolumeProvider2.java @@ -0,0 +1,151 @@ +/* + * Copyright 2018 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.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; +import android.media.update.ApiLoader; +import android.media.update.VolumeProvider2Provider; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Handles requests to adjust or set the volume on a session. This is also used + * to push volume updates back to the session. The provider must call + * {@link #setCurrentVolume(int)} each time the volume being provided changes. + * <p> + * You can set a volume provider on a session by calling + * {@link MediaSession2#setPlayer(MediaPlayerInterface, VolumeProvider2)}. + * + * @hide + */ +public abstract class VolumeProvider2 { + + /** + * @hide + */ + @IntDef({VOLUME_CONTROL_FIXED, VOLUME_CONTROL_RELATIVE, VOLUME_CONTROL_ABSOLUTE}) + @Retention(RetentionPolicy.SOURCE) + public @interface ControlType {} + + /** + * The volume is fixed and can not be modified. Requests to change volume + * should be ignored. + */ + public static final int VOLUME_CONTROL_FIXED = 0; + + /** + * The volume control uses relative adjustment via + * {@link #onAdjustVolume(int)}. Attempts to set the volume to a specific + * value should be ignored. + */ + public static final int VOLUME_CONTROL_RELATIVE = 1; + + /** + * The volume control uses an absolute value. It may be adjusted using + * {@link #onAdjustVolume(int)} or set directly using + * {@link #onSetVolumeTo(int)}. + */ + public static final int VOLUME_CONTROL_ABSOLUTE = 2; + + private final VolumeProvider2Provider mProvider; + + /** + * Create a new volume provider for handling volume events. You must specify + * the type of volume control, the maximum volume that can be used, and the + * current volume on the output. + * + * @param controlType The method for controlling volume that is used by this provider. + * @param maxVolume The maximum allowed volume. + * @param currentVolume The current volume on the output. + */ + public VolumeProvider2(@NonNull Context context, @ControlType int controlType, + int maxVolume, int currentVolume) { + mProvider = ApiLoader.getProvider(context).createVolumeProvider2( + context, this, controlType, maxVolume, currentVolume); + } + + /** + * @hide + */ + @SystemApi + public VolumeProvider2Provider getProvider() { + return mProvider; + } + + /** + * Get the volume control type that this volume provider uses. + * + * @return The volume control type for this volume provider + */ + @ControlType + public final int getControlType() { + return mProvider.getControlType_impl(); + } + + /** + * Get the maximum volume this provider allows. + * + * @return The max allowed volume. + */ + public final int getMaxVolume() { + return mProvider.getMaxVolume_impl(); + } + + /** + * Gets the current volume. This will be the last value set by + * {@link #setCurrentVolume(int)}. + * + * @return The current volume. + */ + public final int getCurrentVolume() { + return mProvider.getCurrentVolume_impl(); + } + + /** + * Notify the system that the current volume has been changed. This must be + * called every time the volume changes to ensure it is displayed properly. + * + * @param currentVolume The current volume on the output. + */ + public final void setCurrentVolume(int currentVolume) { + mProvider.setCurrentVolume_impl(currentVolume); + } + + /** + * Override to handle requests to set the volume of the current output. + * After the volume has been modified {@link #setCurrentVolume} must be + * called to notify the system. + * + * @param volume The volume to set the output to. + */ + public void onSetVolumeTo(int volume) { } + + /** + * Override to handle requests to adjust the volume of the current output. + * Direction will be one of {@link AudioManager#ADJUST_LOWER}, + * {@link AudioManager#ADJUST_RAISE}, {@link AudioManager#ADJUST_SAME}. + * After the volume has been modified {@link #setCurrentVolume} must be + * called to notify the system. + * + * @param direction The direction to change the volume in. + */ + public void onAdjustVolume(int direction) { } +} diff --git a/media/java/android/media/audiofx/Visualizer.java b/media/java/android/media/audiofx/Visualizer.java index 0fe7246e85c4..f2b4fe093248 100644 --- a/media/java/android/media/audiofx/Visualizer.java +++ b/media/java/android/media/audiofx/Visualizer.java @@ -546,22 +546,39 @@ public class Visualizer { /** * Method called when a new waveform capture is available. * <p>Data in the waveform buffer is valid only within the scope of the callback. - * Applications which needs access to the waveform data after returning from the callback + * Applications which need access to the waveform data after returning from the callback * should make a copy of the data instead of holding a reference. * @param visualizer Visualizer object on which the listener is registered. * @param waveform array of bytes containing the waveform representation. - * @param samplingRate sampling rate of the audio visualized. + * @param samplingRate sampling rate of the visualized audio. */ void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate); /** * Method called when a new frequency capture is available. * <p>Data in the fft buffer is valid only within the scope of the callback. - * Applications which needs access to the fft data after returning from the callback + * Applications which need access to the fft data after returning from the callback * should make a copy of the data instead of holding a reference. + * + * <p>In order to obtain magnitude and phase values the following formulas can + * be used: + * <pre class="prettyprint"> + * for (int i = 0; i < fft.size(); i += 2) { + * float magnitude = (float)Math.hypot(fft[i], fft[i + 1]); + * float phase = (float)Math.atan2(fft[i + 1], fft[i]); + * }</pre> * @param visualizer Visualizer object on which the listener is registered. * @param fft array of bytes containing the frequency representation. - * @param samplingRate sampling rate of the audio visualized. + * The fft array only contains the first half of the actual + * FFT spectrum (frequencies up to Nyquist frequency), exploiting + * the symmetry of the spectrum. For each frequencies bin <code>i</code>: + * <ul> + * <li>the element at index <code>2*i</code> in the array contains + * the real part of a complex number,</li> + * <li>the element at index <code>2*i+1</code> contains the imaginary + * part of the complex number.</li> + * </ul> + * @param samplingRate sampling rate of the visualized audio. */ void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate); } diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index 7e88c277cc23..219063564132 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -89,6 +89,8 @@ public class AudioPolicy { private AudioPolicyFocusListener mFocusListener; + private final AudioPolicyVolumeCallback mVolCb; + private Context mContext; private AudioPolicyConfig mConfig; @@ -99,12 +101,15 @@ public class AudioPolicy { public boolean hasFocusListener() { return mFocusListener != null; } /** @hide */ public boolean isFocusPolicy() { return mIsFocusPolicy; } + /** @hide */ + public boolean isVolumeController() { return mVolCb != null; } /** * The parameter is guaranteed non-null through the Builder */ private AudioPolicy(AudioPolicyConfig config, Context context, Looper looper, - AudioPolicyFocusListener fl, AudioPolicyStatusListener sl, boolean isFocusPolicy) { + AudioPolicyFocusListener fl, AudioPolicyStatusListener sl, boolean isFocusPolicy, + AudioPolicyVolumeCallback vc) { mConfig = config; mStatus = POLICY_STATUS_UNREGISTERED; mContext = context; @@ -120,6 +125,7 @@ public class AudioPolicy { mFocusListener = fl; mStatusListener = sl; mIsFocusPolicy = isFocusPolicy; + mVolCb = vc; } /** @@ -134,6 +140,7 @@ public class AudioPolicy { private AudioPolicyFocusListener mFocusListener; private AudioPolicyStatusListener mStatusListener; private boolean mIsFocusPolicy = false; + private AudioPolicyVolumeCallback mVolCb; /** * Constructs a new Builder with no audio mixes. @@ -208,6 +215,22 @@ public class AudioPolicy { mStatusListener = l; } + @SystemApi + /** + * Sets the callback to receive all volume key-related events. + * The callback will only be called if the device is configured to handle volume events + * in the PhoneWindowManager (see config_handleVolumeKeysInWindowManager) + * @param vc + * @return the same Builder instance. + */ + public Builder setAudioPolicyVolumeCallback(@NonNull AudioPolicyVolumeCallback vc) { + if (vc == null) { + throw new IllegalArgumentException("Invalid null volume callback"); + } + mVolCb = vc; + return this; + } + /** * Combines all of the attributes that have been set on this {@code Builder} and returns a * new {@link AudioPolicy} object. @@ -229,7 +252,7 @@ public class AudioPolicy { + "an AudioPolicyFocusListener"); } return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext, mLooper, - mFocusListener, mStatusListener, mIsFocusPolicy); + mFocusListener, mStatusListener, mIsFocusPolicy, mVolCb); } } @@ -440,9 +463,9 @@ public class AudioPolicy { * Only ever called if the {@link AudioPolicy} was built with * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to {@code true}. * @param afi information about the focus request and the requester - * @param requestResult the result that was returned synchronously by the framework to the - * application, {@link #AUDIOFOCUS_REQUEST_FAILED},or - * {@link #AUDIOFOCUS_REQUEST_DELAYED}. + * @param requestResult deprecated after the addition of + * {@link AudioManager#setFocusRequestResult(AudioFocusInfo, int, AudioPolicy)} + * in Android P, always equal to {@link #AUDIOFOCUS_REQUEST_GRANTED}. */ public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {} /** @@ -455,6 +478,23 @@ public class AudioPolicy { public void onAudioFocusAbandon(AudioFocusInfo afi) {} } + @SystemApi + /** + * Callback class to receive volume change-related events. + * See {@link #Builder.setAudioPolicyVolumeCallback(AudioPolicyCallback)} to configure the + * {@link AudioPolicy} to receive those events. + * + */ + public static abstract class AudioPolicyVolumeCallback { + /** @hide */ + public AudioPolicyVolumeCallback() {} + /** + * Called when volume key-related changes are triggered, on the key down event. + * @param adjustment the type of volume adjustment for the key. + */ + public void onVolumeAdjustment(@AudioManager.VolumeAdjustment int adjustment) {} + } + private void onPolicyStatusChange() { AudioPolicyStatusListener l; synchronized (mLock) { @@ -494,7 +534,7 @@ public class AudioPolicy { sendMsg(MSG_FOCUS_REQUEST, afi, requestResult); if (DEBUG) { Log.v(TAG, "notifyAudioFocusRequest: pack=" + afi.getPackageName() + " client=" - + afi.getClientId() + "reqRes=" + requestResult); + + afi.getClientId() + " gen=" + afi.getGen()); } } @@ -517,6 +557,13 @@ public class AudioPolicy { } } } + + public void notifyVolumeAdjust(int adjustment) { + sendMsg(MSG_VOL_ADJUST, null /* ignored */, adjustment); + if (DEBUG) { + Log.v(TAG, "notifyVolumeAdjust: " + adjustment); + } + } }; //================================================== @@ -528,6 +575,7 @@ public class AudioPolicy { private final static int MSG_MIX_STATE_UPDATE = 3; private final static int MSG_FOCUS_REQUEST = 4; private final static int MSG_FOCUS_ABANDON = 5; + private final static int MSG_VOL_ADJUST = 6; private class EventHandler extends Handler { public EventHandler(AudioPolicy ap, Looper looper) { @@ -571,6 +619,13 @@ public class AudioPolicy { Log.e(TAG, "Invalid null focus listener for focus abandon event"); } break; + case MSG_VOL_ADJUST: + if (mVolCb != null) { + mVolCb.onVolumeAdjustment(msg.arg1); + } else { // should never be null, but don't crash + Log.e(TAG, "Invalid null volume event"); + } + break; default: Log.e(TAG, "Unknown event " + msg.what); } diff --git a/media/java/android/media/audiopolicy/IAudioPolicyCallback.aidl b/media/java/android/media/audiopolicy/IAudioPolicyCallback.aidl index 86abbb4dc8d9..107e7cd59ca2 100644 --- a/media/java/android/media/audiopolicy/IAudioPolicyCallback.aidl +++ b/media/java/android/media/audiopolicy/IAudioPolicyCallback.aidl @@ -31,4 +31,7 @@ oneway interface IAudioPolicyCallback { // callback for mix activity status update void notifyMixStateUpdate(in String regId, int state); + + // callback for volume events + void notifyVolumeAdjust(int adjustment); } diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java index c9b096fb124c..2bccd884bea4 100644 --- a/media/java/android/media/browse/MediaBrowser.java +++ b/media/java/android/media/browse/MediaBrowser.java @@ -494,7 +494,7 @@ public final class MediaBrowser { sub = new Subscription(); mSubscriptions.put(parentId, sub); } - sub.putCallback(options, callback); + sub.putCallback(mContext, options, callback); // If we are connected, tell the service that we are watching. If we aren't connected, // the service will be told when we connect. @@ -671,7 +671,8 @@ public final class MediaBrowser { final Subscription subscription = mSubscriptions.get(parentId); if (subscription != null) { // Tell the app. - SubscriptionCallback subscriptionCallback = subscription.getCallback(options); + SubscriptionCallback subscriptionCallback = + subscription.getCallback(mContext, options); if (subscriptionCallback != null) { List<MediaItem> data = list == null ? null : list.getList(); if (options == null) { @@ -1141,7 +1142,10 @@ public final class MediaBrowser { return mCallbacks; } - public SubscriptionCallback getCallback(Bundle options) { + public SubscriptionCallback getCallback(Context context, Bundle options) { + if (options != null) { + options.setClassLoader(context.getClassLoader()); + } for (int i = 0; i < mOptionsList.size(); ++i) { if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { return mCallbacks.get(i); @@ -1150,7 +1154,10 @@ public final class MediaBrowser { return null; } - public void putCallback(Bundle options, SubscriptionCallback callback) { + public void putCallback(Context context, Bundle options, SubscriptionCallback callback) { + if (options != null) { + options.setClassLoader(context.getClassLoader()); + } for (int i = 0; i < mOptionsList.size(); ++i) { if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { mCallbacks.set(i, callback); diff --git a/media/java/android/media/midi/package.html b/media/java/android/media/midi/package.html index 8c1010d610fe..33c54900cd07 100644 --- a/media/java/android/media/midi/package.html +++ b/media/java/android/media/midi/package.html @@ -138,9 +138,10 @@ int numOutputs = info.getOutputPortCount(); </pre> -<p>Note that “input” and “output” are from the standpoint of the device. So a -synthesizer will have an “input” port that receives messages. A keyboard will -have an “output” port that sends messages.</p> +<p>Note that “input” and “output” directions reflect the point of view +of the MIDI device itself, not your app. +For example, to send MIDI notes to a synthesizer, open the synth's INPUT port. +To receive notes from a keyboard, open the keyboard's OUTPUT port.</p> <p>The MidiDeviceInfo has a bundle of properties.</p> @@ -359,8 +360,10 @@ public class MidiSynthDeviceService extends MidiDeviceService { <p>MIDI devices can be connected to Android using Bluetooth LE.</p> <p>Before using the device, the app must scan for available BTLE devices and then allow -the user to connect. An example program -will be provided so look for it on the Android developer website.</p> +the user to connect. +See the Android developer website for an +<a href="https://source.android.com/devices/audio/midi_test#apps" target="_blank">example +program</a>.</p> <h2 id=btle_location_permissions>Request Location Permission for BTLE</h2> diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 9f2c08e5c6ae..aa0d0cc090bc 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -20,8 +20,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; import android.app.Activity; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.media.projection.IMediaProjection; import android.os.Handler; import android.os.IBinder; @@ -71,8 +73,11 @@ public final class MediaProjectionManager { */ public Intent createScreenCaptureIntent() { Intent i = new Intent(); - i.setClassName("com.android.systemui", - "com.android.systemui.media.MediaProjectionPermissionActivity"); + final ComponentName mediaProjectionPermissionDialogComponent = + ComponentName.unflattenFromString(mContext.getResources().getString( + com.android.internal.R.string + .config_mediaProjectionPermissionDialogComponent)); + i.setComponent(mediaProjectionPermissionDialogComponent); return i; } diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl index 5fcb4304a42a..2d365d072899 100644 --- a/media/java/android/media/session/ISessionManager.aidl +++ b/media/java/android/media/session/ISessionManager.aidl @@ -17,6 +17,7 @@ package android.media.session; import android.content.ComponentName; import android.media.IRemoteVolumeController; +import android.media.ISessionTokensListener; import android.media.session.IActiveSessionsListener; import android.media.session.ICallback; import android.media.session.IOnMediaKeyListener; @@ -49,4 +50,13 @@ interface ISessionManager { void setCallback(in ICallback callback); void setOnVolumeKeyLongPressListener(in IOnVolumeKeyLongPressListener listener); void setOnMediaKeyListener(in IOnMediaKeyListener listener); + + // MediaSession2 + boolean onSessionCreated(in Bundle sessionToken); + void onSessionDestroyed(in Bundle sessionToken); + List<Bundle> getSessionTokens(boolean activeSessionOnly, boolean sessionServiceOnly); + + void addSessionTokensListener(in ISessionTokensListener listener, int userId, + String packageName); + void removeSessionTokensListener(in ISessionTokensListener listener); } diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 1291dfb59d2c..b8184a0789b6 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -119,7 +119,7 @@ public final class MediaSession { private final ISession mBinder; private final CallbackStub mCbStub; - private CallbackMessageHandler mCallback; + private CallbackMessageHandler mCallbackHandler; private VolumeProvider mVolumeProvider; private PlaybackState mPlaybackState; @@ -194,24 +194,22 @@ public final class MediaSession { */ public void setCallback(@Nullable Callback callback, @Nullable Handler handler) { synchronized (mLock) { + if (mCallbackHandler != null) { + // We're updating the callback, clear the session from the old one. + mCallbackHandler.mCallback.mSession = null; + mCallbackHandler.removeCallbacksAndMessages(null); + } if (callback == null) { - if (mCallback != null) { - mCallback.mCallback.mSession = null; - } - mCallback = null; + mCallbackHandler = null; return; } - if (mCallback != null) { - // We're updating the callback, clear the session from the old one. - mCallback.mCallback.mSession = null; - } if (handler == null) { handler = new Handler(); } callback.mSession = this; CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(), callback); - mCallback = msgHandler; + mCallbackHandler = msgHandler; } } @@ -636,8 +634,8 @@ public final class MediaSession { private void postToCallback(int what, Object obj, Bundle extras) { synchronized (mLock) { - if (mCallback != null) { - mCallback.post(what, obj, extras); + if (mCallbackHandler != null) { + mCallbackHandler.post(what, obj, extras); } } } diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java index b215825cbfb4..454113cb78ac 100644 --- a/media/java/android/media/session/MediaSessionManager.java +++ b/media/java/android/media/session/MediaSessionManager.java @@ -16,6 +16,7 @@ package android.media.session; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -25,7 +26,11 @@ import android.content.ComponentName; import android.content.Context; import android.media.AudioManager; import android.media.IRemoteVolumeController; -import android.media.session.ISessionManager; +import android.media.ISessionTokensListener; +import android.media.MediaSession2; +import android.media.MediaSessionService2; +import android.media.SessionToken2; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -38,7 +43,9 @@ import android.util.Log; import android.view.KeyEvent; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.Executor; /** * Provides support for interacting with {@link MediaSession media sessions} @@ -66,6 +73,8 @@ public final class MediaSessionManager { private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners = new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>(); + private final ArrayMap<OnSessionTokensChangedListener, SessionTokensChangedWrapper> + mSessionTokensListener = new ArrayMap<>(); private final Object mLock = new Object(); private final ISessionManager mService; @@ -331,6 +340,203 @@ public final class MediaSessionManager { } /** + * Called when a {@link MediaSession2} is created. + * @hide + */ + // TODO(jaewan): System API + public boolean onSessionCreated(@NonNull SessionToken2 token) { + if (token == null) { + return false; + } + try { + return mService.onSessionCreated(token.toBundle()); + } catch (RemoteException e) { + Log.wtf(TAG, "Cannot communicate with the service.", e); + } + return false; + } + + /** Called when a {@link MediaSession2} is destroyed. + * @hide + */ + // TODO(jaewan): System API + public void onSessionDestroyed(@NonNull SessionToken2 token) { + if (token == null) { + return; + } + try { + mService.onSessionDestroyed(token.toBundle()); + } catch (RemoteException e) { + Log.wtf(TAG, "Cannot communicate with the service.", e); + } + } + + /** + * Get {@link List} of {@link SessionToken2} whose sessions are active now. This list represents + * active sessions regardless of whether they're {@link MediaSession2} or + * {@link MediaSessionService2}. + * <p> + * 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. + * + * @return list of tokens + * @hide + */ + // TODO(jaewan): Unhide + public List<SessionToken2> getActiveSessionTokens() { + try { + List<Bundle> bundles = mService.getSessionTokens( + /* activeSessionOnly */ true, /* sessionServiceOnly */ false); + return toTokenList(mContext, bundles); + } catch (RemoteException e) { + Log.wtf(TAG, "Cannot communicate with the service.", e); + return Collections.emptyList(); + } + } + + /** + * Get {@link List} of {@link SessionToken2} for {@link MediaSessionService2} regardless of their + * activeness. This list represents media apps that support background playback. + * <p> + * 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. + * + * @return list of tokens + * @hide + */ + // TODO(jaewan): Unhide + public List<SessionToken2> getSessionServiceTokens() { + try { + List<Bundle> bundles = mService.getSessionTokens( + /* activeSessionOnly */ false, /* sessionServiceOnly */ true); + return toTokenList(mContext, bundles); + } catch (RemoteException e) { + Log.wtf(TAG, "Cannot communicate with the service.", e); + return Collections.emptyList(); + } + } + + /** + * Get all {@link SessionToken2}s. This is the combined list of {@link #getActiveSessionTokens()} + * and {@link #getSessionServiceTokens}. + * <p> + * 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. + * + * @return list of tokens + * @see #getActiveSessionTokens + * @see #getSessionServiceTokens + * @hide + */ + // TODO(jaewan): Unhide + public List<SessionToken2> getAllSessionTokens() { + try { + List<Bundle> bundles = mService.getSessionTokens( + /* activeSessionOnly */ false, /* sessionServiceOnly */ false); + return toTokenList(mContext, bundles); + } catch (RemoteException e) { + Log.wtf(TAG, "Cannot communicate with the service.", e); + return Collections.emptyList(); + } + } + + /** + * Add a listener to be notified when the {@link #getAllSessionTokens()} changes. + * <p> + * 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. + * + * @param executor executor to run this command + * @param listener The listener to add. + * @hide + */ + // TODO(jaewan): Unhide + public void addOnSessionTokensChangedListener(@NonNull @CallbackExecutor Executor executor, + @NonNull OnSessionTokensChangedListener listener) { + addOnSessionTokensChangedListener(UserHandle.myUserId(), executor, listener); + } + + /** + * Add a listener to be notified when the {@link #getAllSessionTokens()} changes. + * <p> + * 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. + * + * @param userId The userId to listen for changes on. + * @param executor executor to run this command + * @param listener The listener to add. + * @hide + */ + public void addOnSessionTokensChangedListener(int userId, + @NonNull @CallbackExecutor Executor executor, + @NonNull OnSessionTokensChangedListener listener) { + if (executor == null) { + throw new IllegalArgumentException("executor may not be null"); + } + if (listener == null) { + throw new IllegalArgumentException("listener may not be null"); + } + synchronized (mLock) { + if (mSessionTokensListener.get(listener) != null) { + Log.w(TAG, "Attempted to add session listener twice, ignoring."); + return; + } + SessionTokensChangedWrapper wrapper = new SessionTokensChangedWrapper( + mContext, executor, listener); + try { + mService.addSessionTokensListener(wrapper.mStub, userId, mContext.getPackageName()); + mSessionTokensListener.put(listener, wrapper); + } catch (RemoteException e) { + Log.e(TAG, "Error in addSessionTokensListener.", e); + } + } + } + + /** + * Stop receiving session token updates on the specified listener. + * + * @param listener The listener to remove. + * @hide + */ + // TODO(jaewan): Unhide + public void removeOnSessionTokensChangedListener( + @NonNull OnSessionTokensChangedListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener may not be null"); + } + synchronized (mLock) { + SessionTokensChangedWrapper wrapper = mSessionTokensListener.remove(listener); + if (wrapper != null) { + try { + mService.removeSessionTokensListener(wrapper.mStub); + } catch (RemoteException e) { + Log.e(TAG, "Error in removeSessionTokensListener.", e); + } finally { + wrapper.release(); + } + } + } + } + + private static List<SessionToken2> toTokenList(Context context, List<Bundle> bundles) { + List<SessionToken2> tokens = new ArrayList<>(); + if (bundles != null) { + for (int i = 0; i < bundles.size(); i++) { + SessionToken2 token = SessionToken2.fromBundle(context, bundles.get(i)); + if (token != null) { + tokens.add(token); + } + } + } + return tokens; + } + + /** * Check if the global priority session is currently active. This can be * used to decide if media keys should be sent to the session or to the app. * @@ -452,6 +658,16 @@ public final class MediaSessionManager { } /** + * Listens for changes to the {@link #getAllSessionTokens()}. This can be added + * using {@link #addOnActiveSessionsChangedListener}. + * @hide + */ + // TODO(jaewan): Unhide + public interface OnSessionTokensChangedListener { + void onSessionTokensChanged(@NonNull List<SessionToken2> tokens); + } + + /** * Listens the volume key long-presses. * @hide */ @@ -577,6 +793,35 @@ public final class MediaSessionManager { } } + private static final class SessionTokensChangedWrapper { + private Context mContext; + private Executor mExecutor; + private OnSessionTokensChangedListener mListener; + + public SessionTokensChangedWrapper(Context context, Executor executor, + OnSessionTokensChangedListener listener) { + mContext = context; + mExecutor = executor; + mListener = listener; + } + + private final ISessionTokensListener.Stub mStub = new ISessionTokensListener.Stub() { + @Override + public void onSessionTokensChanged(final List<Bundle> bundles) { + mExecutor.execute(() -> { + List<SessionToken2> tokens = toTokenList(mContext, bundles); + mListener.onSessionTokensChanged(tokens); + }); + } + }; + + private void release() { + mListener = null; + mContext = null; + mExecutor = null; + } + } + private static final class OnVolumeKeyLongPressListenerImpl extends IOnVolumeKeyLongPressListener.Stub { private OnVolumeKeyLongPressListener mListener; diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java index 8283c8b967e8..17d16b896679 100644 --- a/media/java/android/media/session/PlaybackState.java +++ b/media/java/android/media/session/PlaybackState.java @@ -17,6 +17,7 @@ package android.media.session; import android.annotation.DrawableRes; import android.annotation.IntDef; +import android.annotation.LongDef; import android.annotation.Nullable; import android.media.RemoteControlClient; import android.os.Bundle; @@ -41,7 +42,7 @@ public final class PlaybackState implements Parcelable { /** * @hide */ - @IntDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, + @LongDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING, ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH, ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE, diff --git a/media/java/android/media/tv/ITvInputHardware.aidl b/media/java/android/media/tv/ITvInputHardware.aidl index 96223ba7bac1..94c1013a837e 100644 --- a/media/java/android/media/tv/ITvInputHardware.aidl +++ b/media/java/android/media/tv/ITvInputHardware.aidl @@ -40,12 +40,6 @@ interface ITvInputHardware { void setStreamVolume(float volume); /** - * Dispatch key event to HDMI service. The events would be automatically converted to - * HDMI CEC commands. If the hardware is not representing an HDMI port, this method will fail. - */ - boolean dispatchKeyEventToHdmi(in KeyEvent event); - - /** * Override default audio sink from audio policy. When override is on, it is * TvInputService's responsibility to adjust to audio configuration change * (for example, when the audio sink becomes unavailable or more desirable diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index 48fb5bffffd3..3bbc2c4e2304 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -703,37 +703,37 @@ public final class TvContract { } /** - * Returns {@code true}, if {@code uri} is a channel URI. + * @return {@code true} if {@code uri} is a channel URI. */ - public static boolean isChannelUri(Uri uri) { + public static boolean isChannelUri(@NonNull Uri uri) { return isChannelUriForTunerInput(uri) || isChannelUriForPassthroughInput(uri); } /** - * Returns {@code true}, if {@code uri} is a channel URI for a tuner input. + * @return {@code true} if {@code uri} is a channel URI for a tuner input. */ - public static boolean isChannelUriForTunerInput(Uri uri) { + public static boolean isChannelUriForTunerInput(@NonNull Uri uri) { return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL); } /** - * Returns {@code true}, if {@code uri} is a channel URI for a pass-through input. + * @return {@code true} if {@code uri} is a channel URI for a pass-through input. */ - public static boolean isChannelUriForPassthroughInput(Uri uri) { + public static boolean isChannelUriForPassthroughInput(@NonNull Uri uri) { return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_PASSTHROUGH); } /** - * Returns {@code true}, if {@code uri} is a program URI. + * @return {@code true} if {@code uri} is a program URI. */ - public static boolean isProgramUri(Uri uri) { + public static boolean isProgramUri(@NonNull Uri uri) { return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_PROGRAM); } /** - * Returns {@code true}, if {@code uri} is a recorded program URI. + * @return {@code true} if {@code uri} is a recorded program URI. */ - public static boolean isRecordedProgramUri(Uri uri) { + public static boolean isRecordedProgramUri(@NonNull Uri uri) { return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_RECORDED_PROGRAM); } @@ -1650,7 +1650,7 @@ public final class TvContract { public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/channel"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "TYPE_" }, value = { TYPE_OTHER, TYPE_NTSC, TYPE_PAL, @@ -1863,7 +1863,7 @@ public final class TvContract { public static final String TYPE_PREVIEW = "TYPE_PREVIEW"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "SERVICE_TYPE_" }, value = { SERVICE_TYPE_OTHER, SERVICE_TYPE_AUDIO_VIDEO, SERVICE_TYPE_AUDIO, @@ -1881,7 +1881,7 @@ public final class TvContract { public static final String SERVICE_TYPE_AUDIO = "SERVICE_TYPE_AUDIO"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "VIDEO_FORMAT_" }, value = { VIDEO_FORMAT_240P, VIDEO_FORMAT_360P, VIDEO_FORMAT_480I, @@ -1930,7 +1930,7 @@ public final class TvContract { public static final String VIDEO_FORMAT_4320P = "VIDEO_FORMAT_4320P"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "VIDEO_RESOLUTION_" }, value = { VIDEO_RESOLUTION_SD, VIDEO_RESOLUTION_ED, VIDEO_RESOLUTION_HD, diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 2eaea6bf623d..143182f83ace 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -1329,7 +1329,6 @@ public final class TvInputManager { * Returns the list of blocked content ratings. * * @return the list of content ratings blocked by the user. - * @hide */ @SystemApi public List<TvContentRating> getBlockedRatings() { @@ -1387,6 +1386,7 @@ public final class TvInputManager { * @hide */ @SystemApi + @RequiresPermission(android.Manifest.permission.READ_CONTENT_RATING_SYSTEMS) public List<TvContentRatingSystemInfo> getTvContentRatingSystemList() { try { return mService.getTvContentRatingSystemList(mUserId); @@ -1551,6 +1551,7 @@ public final class TvInputManager { * @hide */ @SystemApi + @RequiresPermission(android.Manifest.permission.CAPTURE_TV_INPUT) public boolean isSingleSessionActive() { try { return mService.isSingleSessionActive(mUserId); @@ -2592,12 +2593,10 @@ public final class TvInputManager { } } + /** @removed */ + @SystemApi public boolean dispatchKeyEventToHdmi(KeyEvent event) { - try { - return mInterface.dispatchKeyEventToHdmi(event); - } catch (RemoteException e) { - throw new RuntimeException(e); - } + return false; } public void overrideAudioSink(int audioType, String audioAddress, int samplingRate, diff --git a/media/java/android/media/update/ApiLoader.java b/media/java/android/media/update/ApiLoader.java new file mode 100644 index 000000000000..b928e9319b18 --- /dev/null +++ b/media/java/android/media/update/ApiLoader.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 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.update; + +import android.content.res.Resources; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +/** + * @hide + */ +public final class ApiLoader { + private static Object sMediaLibrary; + + private static final String UPDATE_PACKAGE = "com.android.media.update"; + private static final String UPDATE_CLASS = "com.android.media.update.ApiFactory"; + private static final String UPDATE_METHOD = "initialize"; + + private ApiLoader() { } + + public static StaticProvider getProvider(Context context) { + try { + return (StaticProvider) getMediaLibraryImpl(context); + } catch (PackageManager.NameNotFoundException | ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + // TODO This method may do I/O; Ensure it does not violate (emit warnings in) strict mode. + private static synchronized Object getMediaLibraryImpl(Context context) + throws PackageManager.NameNotFoundException, ReflectiveOperationException { + if (sMediaLibrary != null) return sMediaLibrary; + + // TODO Figure out when to use which package (query media update service) + int flags = Build.IS_DEBUGGABLE ? 0 : PackageManager.MATCH_FACTORY_ONLY; + Context libContext = context.createApplicationContext( + context.getPackageManager().getPackageInfo(UPDATE_PACKAGE, flags).applicationInfo, + Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); + sMediaLibrary = libContext.getClassLoader() + .loadClass(UPDATE_CLASS) + .getMethod(UPDATE_METHOD, Resources.class, Resources.Theme.class) + .invoke(null, libContext.getResources(), libContext.getTheme()); + return sMediaLibrary; + } +} diff --git a/media/java/android/media/update/MediaBrowser2Provider.java b/media/java/android/media/update/MediaBrowser2Provider.java new file mode 100644 index 000000000000..f2e73132a13a --- /dev/null +++ b/media/java/android/media/update/MediaBrowser2Provider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 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.update; + +import android.os.Bundle; + +/** + * @hide + */ +public interface MediaBrowser2Provider extends MediaController2Provider { + void getLibraryRoot_impl(Bundle rootHints); + + void subscribe_impl(String parentId, Bundle options); + void unsubscribe_impl(String parentId, Bundle options); + + void getItem_impl(String mediaId); + void getChildren_impl(String parentId, int page, int pageSize, Bundle extras); + void search_impl(String query, Bundle options); + void getSearchResult_impl(String query, int page, int pageSize, Bundle extras); +} diff --git a/media/java/android/media/update/MediaControlView2Provider.java b/media/java/android/media/update/MediaControlView2Provider.java new file mode 100644 index 000000000000..ebde3fefac36 --- /dev/null +++ b/media/java/android/media/update/MediaControlView2Provider.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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.update; + +import android.annotation.SystemApi; +import android.media.session.MediaController; +import android.util.AttributeSet; +import android.view.View; + +/** + * Interface for connecting the public API to an updatable implementation. + * + * Each instance object is connected to one corresponding updatable object which implements the + * runtime behavior of that class. There should a corresponding provider method for all public + * methods. + * + * All methods behave as per their namesake in the public API. + * + * @see android.widget.MediaControlView2 + * + * @hide + */ +// TODO @SystemApi +public interface MediaControlView2Provider extends ViewGroupProvider { + void initialize(AttributeSet attrs, int defStyleAttr, int defStyleRes); + + void setController_impl(MediaController controller); + void setButtonVisibility_impl(int button, int visibility); + void requestPlayButtonFocus_impl(); +} diff --git a/media/java/android/media/update/MediaController2Provider.java b/media/java/android/media/update/MediaController2Provider.java new file mode 100644 index 000000000000..c492d3072c78 --- /dev/null +++ b/media/java/android/media/update/MediaController2Provider.java @@ -0,0 +1,77 @@ +/* + * Copyright 2018 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.update; + +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.media.AudioAttributes; +import android.media.MediaController2.PlaybackInfo; +import android.media.MediaItem2; +import android.media.MediaSession2.Command; +import android.media.MediaSession2.PlaylistParams; +import android.media.PlaybackState2; +import android.media.Rating2; +import android.media.SessionToken2; +import android.net.Uri; +import android.os.Bundle; +import android.os.ResultReceiver; + +import java.util.List; + +/** + * @hide + */ +public interface MediaController2Provider extends TransportControlProvider { + void initialize(); + + void close_impl(); + SessionToken2 getSessionToken_impl(); + boolean isConnected_impl(); + + PendingIntent getSessionActivity_impl(); + int getRatingType_impl(); + + void setVolumeTo_impl(int value, int flags); + void adjustVolume_impl(int direction, int flags); + PlaybackInfo getPlaybackInfo_impl(); + + void prepareFromUri_impl(Uri uri, Bundle extras); + void prepareFromSearch_impl(String query, Bundle extras); + void prepareMediaId_impl(String mediaId, Bundle extras); + void playFromSearch_impl(String query, Bundle extras); + void playFromUri_impl(Uri uri, Bundle extras); + void playFromMediaId_impl(String mediaId, Bundle extras); + + void setRating_impl(Rating2 rating); + void sendCustomCommand_impl(Command command, Bundle args, ResultReceiver cb); + List<MediaItem2> getPlaylist_impl(); + + void removePlaylistItem_impl(MediaItem2 index); + void addPlaylistItem_impl(int index, MediaItem2 item); + + PlaylistParams getPlaylistParams_impl(); + void setPlaylistParams_impl(PlaylistParams params); + PlaybackState2 getPlaybackState_impl(); + + interface PlaybackInfoProvider { + int getPlaybackType_impl(); + AudioAttributes getAudioAttributes_impl(); + int getControlType_impl(); + int getMaxVolume_impl(); + int getCurrentVolume_impl(); + } +} diff --git a/media/java/android/media/update/MediaItem2Provider.java b/media/java/android/media/update/MediaItem2Provider.java new file mode 100644 index 000000000000..2970f0ec41f8 --- /dev/null +++ b/media/java/android/media/update/MediaItem2Provider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 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.update; + +import android.media.DataSourceDesc; +import android.media.MediaMetadata2; +import android.os.Bundle; + +/** + * @hide + */ +// TODO(jaewan): SystemApi +public interface MediaItem2Provider { + Bundle toBundle_impl(); + String toString_impl(); + int getFlags_impl(); + boolean isBrowsable_impl(); + boolean isPlayable_impl(); + void setMetadata_impl(MediaMetadata2 metadata); + MediaMetadata2 getMetadata_impl(); + String getMediaId_impl(); + DataSourceDesc getDataSourceDesc_impl(); +} diff --git a/media/java/android/media/update/MediaLibraryService2Provider.java b/media/java/android/media/update/MediaLibraryService2Provider.java new file mode 100644 index 000000000000..923551a226b6 --- /dev/null +++ b/media/java/android/media/update/MediaLibraryService2Provider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 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.update; + +import android.annotation.SystemApi; +import android.media.MediaLibraryService2.MediaLibrarySession; +import android.media.MediaLibraryService2.MediaLibrarySessionCallback; +import android.media.MediaSession2.ControllerInfo; +import android.os.Bundle; + +/** + * @hide + */ +// TODO: @SystemApi +public interface MediaLibraryService2Provider extends MediaSessionService2Provider { + // Nothing new for now + + interface MediaLibrarySessionProvider extends MediaSession2Provider { + void notifyChildrenChanged_impl(ControllerInfo controller, String parentId, Bundle options); + void notifyChildrenChanged_impl(String parentId, Bundle options); + } + + interface LibraryRootProvider { + String getRootId_impl(); + Bundle getExtras_impl(); + } +} diff --git a/media/java/android/media/update/MediaMetadata2Provider.java b/media/java/android/media/update/MediaMetadata2Provider.java new file mode 100644 index 000000000000..55ac43d797d4 --- /dev/null +++ b/media/java/android/media/update/MediaMetadata2Provider.java @@ -0,0 +1,37 @@ +package android.media.update; + +import android.graphics.Bitmap; +import android.media.MediaMetadata2; +import android.media.MediaMetadata2.Builder; +import android.media.Rating2; +import android.os.Bundle; + +import java.util.Set; + +/** + * @hide + */ +// TODO(jaewan): SystemApi +public interface MediaMetadata2Provider { + boolean containsKey_impl(String key); + CharSequence getText_impl(String key); + String getMediaId_impl(); + String getString_impl(String key); + long getLong_impl(String key); + Rating2 getRating_impl(String key); + Bundle toBundle_impl(); + Set<String> keySet_impl(); + int size_impl(); + Bitmap getBitmap_impl(String key); + Bundle getExtra_impl(); + + interface BuilderProvider { + Builder putText_impl(String key, CharSequence value); + Builder putString_impl(String key, String value); + Builder putLong_impl(String key, long value); + Builder putRating_impl(String key, Rating2 value); + Builder putBitmap_impl(String key, Bitmap value); + Builder setExtra_impl(Bundle bundle); + MediaMetadata2 build_impl(); + } +} diff --git a/media/java/android/media/update/MediaSession2Provider.java b/media/java/android/media/update/MediaSession2Provider.java new file mode 100644 index 000000000000..41162e0a15ea --- /dev/null +++ b/media/java/android/media/update/MediaSession2Provider.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018 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.update; + +import android.app.PendingIntent; +import android.media.MediaItem2; +import android.media.MediaMetadata2; +import android.media.MediaPlayerInterface; +import android.media.MediaPlayerInterface.PlaybackListener; +import android.media.MediaSession2; +import android.media.MediaSession2.Command; +import android.media.MediaSession2.CommandButton; +import android.media.MediaSession2.CommandButton.Builder; +import android.media.MediaSession2.CommandGroup; +import android.media.MediaSession2.ControllerInfo; +import android.media.MediaSession2.PlaylistParams; +import android.media.MediaSession2.SessionCallback; +import android.media.SessionToken2; +import android.media.VolumeProvider2; +import android.os.Bundle; +import android.os.ResultReceiver; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * @hide + */ +// TODO: @SystemApi +public interface MediaSession2Provider extends TransportControlProvider { + void close_impl(); + void setPlayer_impl(MediaPlayerInterface player); + void setPlayer_impl(MediaPlayerInterface player, VolumeProvider2 volumeProvider); + MediaPlayerInterface getPlayer_impl(); + SessionToken2 getToken_impl(); + List<ControllerInfo> getConnectedControllers_impl(); + void setCustomLayout_impl(ControllerInfo controller, List<CommandButton> layout); + void setAudioFocusRequest_impl(int focusGain); + + void setAllowedCommands_impl(ControllerInfo controller, CommandGroup commands); + void notifyMetadataChanged_impl(); + void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args, + ResultReceiver receiver); + void sendCustomCommand_impl(Command command, Bundle args); + void setPlaylist_impl(List<MediaItem2> playlist); + List<MediaItem2> getPlaylist_impl(); + void setPlaylistParams_impl(PlaylistParams params); + PlaylistParams getPlaylistParams_impl(); + + void addPlaybackListener_impl(Executor executor, PlaybackListener listener); + void removePlaybackListener_impl(PlaybackListener listener); + + interface CommandProvider { + int getCommandCode_impl(); + String getCustomCommand_impl(); + Bundle getExtra_impl(); + Bundle toBundle_impl(); + + boolean equals_impl(Object ob); + int hashCode_impl(); + } + + interface CommandGroupProvider { + void addCommand_impl(Command command); + void addAllPredefinedCommands_impl(); + void removeCommand_impl(Command command); + boolean hasCommand_impl(Command command); + boolean hasCommand_impl(int code); + Bundle toBundle_impl(); + } + + interface CommandButtonProvider { + Command getCommand_impl(); + int getIconResId_impl(); + String getDisplayName_impl(); + Bundle getExtra_impl(); + boolean isEnabled_impl(); + + interface BuilderProvider { + Builder setCommand_impl(Command command); + Builder setIconResId_impl(int resId); + Builder setDisplayName_impl(String displayName); + Builder setEnabled_impl(boolean enabled); + Builder setExtra_impl(Bundle extra); + CommandButton build_impl(); + } + } + + interface ControllerInfoProvider { + String getPackageName_impl(); + int getUid_impl(); + boolean isTrusted_impl(); + int hashCode_impl(); + boolean equals_impl(ControllerInfoProvider obj); + } + + interface PlaylistParamsProvider { + int getRepeatMode_impl(); + int getShuffleMode_impl(); + MediaMetadata2 getPlaylistMetadata_impl(); + Bundle toBundle_impl(); + } + + interface BuilderBaseProvider<T extends MediaSession2, C extends SessionCallback> { + void setVolumeProvider_impl(VolumeProvider2 volumeProvider); + void setRatingType_impl(int type); + void setSessionActivity_impl(PendingIntent pi); + void setId_impl(String id); + void setSessionCallback_impl(Executor executor, C callback); + T build_impl(); + } +} diff --git a/media/java/android/media/update/MediaSessionService2Provider.java b/media/java/android/media/update/MediaSessionService2Provider.java new file mode 100644 index 000000000000..42e75871e12f --- /dev/null +++ b/media/java/android/media/update/MediaSessionService2Provider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 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.update; + +import android.annotation.SystemApi; +import android.app.Notification; +import android.content.Intent; +import android.media.MediaSession2; +import android.media.MediaSessionService2.MediaNotification; +import android.media.PlaybackState2; +import android.os.IBinder; + +/** + * @hide + */ +public interface MediaSessionService2Provider { + MediaSession2 getSession_impl(); + MediaNotification onUpdateNotification_impl(PlaybackState2 state); + + // Service + void onCreate_impl(); + IBinder onBind_impl(Intent intent); + + interface MediaNotificationProvider { + int getNotificationId_impl(); + Notification getNotification_impl(); + } +} diff --git a/media/java/android/media/update/PlaybackState2Provider.java b/media/java/android/media/update/PlaybackState2Provider.java new file mode 100644 index 000000000000..2875e98bb8a4 --- /dev/null +++ b/media/java/android/media/update/PlaybackState2Provider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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.update; + +import android.os.Bundle; + +/** + * @hide + */ +// TODO(jaewan): @SystemApi +public interface PlaybackState2Provider { + String toString_impl(); + + int getState_impl(); + + long getPosition_impl(); + + long getBufferedPosition_impl(); + + float getPlaybackSpeed_impl(); + + CharSequence getErrorMessage_impl(); + + long getLastPositionUpdateTime_impl(); + + long getCurrentPlaylistItemIndex_impl(); + + Bundle toBundle_impl(); +} diff --git a/media/java/android/media/VolumeShaper.aidl b/media/java/android/media/update/ProviderCreator.java index ecf6a8f5d0f8..f5f3e470812f 100644 --- a/media/java/android/media/VolumeShaper.aidl +++ b/media/java/android/media/update/ProviderCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 The Android Open Source Project + * Copyright 2018 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. @@ -14,8 +14,10 @@ * limitations under the License. */ -package android.media; +package android.media.update; -parcelable VolumeShaper.Configuration; -parcelable VolumeShaper.Operation; -parcelable VolumeShaper.State;
\ No newline at end of file +/** @hide */ +@FunctionalInterface +public interface ProviderCreator<T, U> { + U createProvider(T instance); +} diff --git a/media/java/android/media/update/Rating2Provider.java b/media/java/android/media/update/Rating2Provider.java new file mode 100644 index 000000000000..8966196890c7 --- /dev/null +++ b/media/java/android/media/update/Rating2Provider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 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.update; + +import android.annotation.SystemApi; +import android.os.Bundle; + +/** + * @hide + */ +// TODO(jaewan): @SystemApi +public interface Rating2Provider { + String toString_impl(); + boolean equals_impl(Object obj); + int hashCode_impl(); + Bundle toBundle_impl(); + boolean isRated_impl(); + int getRatingStyle_impl(); + boolean hasHeart_impl(); + boolean isThumbUp_impl(); + float getStarRating_impl(); + float getPercentRating_impl(); +}
\ No newline at end of file diff --git a/media/java/android/media/update/SessionPlayer2Provider.java b/media/java/android/media/update/SessionPlayer2Provider.java new file mode 100644 index 000000000000..e068c211f6d1 --- /dev/null +++ b/media/java/android/media/update/SessionPlayer2Provider.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 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.update; + +import android.media.AudioAttributes; +import android.media.MediaItem2; +import android.media.MediaPlayer2; +import android.media.MediaPlayerInterface.PlaybackListener; +import android.media.MediaSession2.PlaylistParams; +import android.media.PlaybackState2; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * @hide + */ +public interface SessionPlayer2Provider { + void play_impl(); + void prepare_impl(); + void pause_impl(); + void stop_impl(); + void skipToPrevious_impl(); + void skipToNext_impl(); + void seekTo_impl(long pos); + void fastForward_impl(); + void rewind_impl(); + PlaybackState2 getPlaybackState_impl(); + void setAudioAttributes_impl(AudioAttributes attributes); + AudioAttributes getAudioAttributes_impl(); + void addPlaylistItem_impl(int index, MediaItem2 item); + void removePlaylistItem_impl(MediaItem2 item); + void setPlaylist_impl(List<MediaItem2> playlist); + List<MediaItem2> getPlaylist_impl(); + void setCurrentPlaylistItem_impl(int index); + void setPlaylistParams_impl(PlaylistParams params); + PlaylistParams getPlaylistParams_impl(); + void addPlaybackListener_impl(Executor executor, PlaybackListener listener); + void removePlaybackListener_impl(PlaybackListener listener); + MediaPlayer2 getPlayer_impl(); +} diff --git a/media/java/android/media/update/SessionToken2Provider.java b/media/java/android/media/update/SessionToken2Provider.java new file mode 100644 index 000000000000..95d6ce07b8a8 --- /dev/null +++ b/media/java/android/media/update/SessionToken2Provider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 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.update; + +import android.os.Bundle; + +/** + * @hide + */ +public interface SessionToken2Provider { + String getPackageName_impl(); + String getId_imp(); + int getType_impl(); + int getUid_impl(); + Bundle toBundle_impl(); + + int hashCode_impl(); + boolean equals_impl(Object obj); + String toString_impl(); +} diff --git a/media/java/android/media/update/StaticProvider.java b/media/java/android/media/update/StaticProvider.java new file mode 100644 index 000000000000..57f04cc88ff5 --- /dev/null +++ b/media/java/android/media/update/StaticProvider.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2017 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.update; + +import android.annotation.Nullable; +import android.app.Notification; +import android.content.Context; +import android.media.DataSourceDesc; +import android.media.MediaBrowser2; +import android.media.MediaBrowser2.BrowserCallback; +import android.media.MediaController2; +import android.media.MediaController2.ControllerCallback; +import android.media.MediaItem2; +import android.media.MediaLibraryService2; +import android.media.MediaLibraryService2.LibraryRoot; +import android.media.MediaLibraryService2.MediaLibrarySession; +import android.media.MediaLibraryService2.MediaLibrarySessionBuilder; +import android.media.MediaLibraryService2.MediaLibrarySessionCallback; +import android.media.MediaMetadata2; +import android.media.MediaPlayerInterface; +import android.media.MediaSession2; +import android.media.MediaSession2.CommandButton.Builder; +import android.media.MediaSession2.PlaylistParams; +import android.media.MediaSession2.SessionCallback; +import android.media.MediaSessionService2; +import android.media.MediaSessionService2.MediaNotification; +import android.media.PlaybackState2; +import android.media.Rating2; +import android.media.SessionPlayer2; +import android.media.SessionToken2; +import android.media.VolumeProvider2; +import android.media.update.MediaLibraryService2Provider.LibraryRootProvider; +import android.media.update.MediaSession2Provider.BuilderBaseProvider; +import android.media.update.MediaSession2Provider.CommandButtonProvider.BuilderProvider; +import android.media.update.MediaSession2Provider.CommandGroupProvider; +import android.media.update.MediaSession2Provider.CommandProvider; +import android.media.update.MediaSession2Provider.ControllerInfoProvider; +import android.media.update.MediaSession2Provider.PlaylistParamsProvider; +import android.media.update.MediaSessionService2Provider.MediaNotificationProvider; +import android.os.Bundle; +import android.os.IInterface; +import android.util.AttributeSet; +import android.widget.MediaControlView2; +import android.widget.VideoView2; + +import java.util.concurrent.Executor; + +/** + * Interface for connecting the public API to an updatable implementation. + * + * This interface provides access to constructors and static methods that are otherwise not directly + * accessible via an implementation object. + * @hide + */ +public interface StaticProvider { + MediaControlView2Provider createMediaControlView2(MediaControlView2 instance, + ViewGroupProvider superProvider, ViewGroupProvider privateProvider, + @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes); + VideoView2Provider createVideoView2(VideoView2 instance, + ViewGroupProvider superProvider, ViewGroupProvider privateProvider, + @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes); + + CommandProvider createMediaSession2Command(MediaSession2.Command instance, + int commandCode, String action, Bundle extra); + MediaSession2.Command fromBundle_MediaSession2Command(Context context, Bundle bundle); + CommandGroupProvider createMediaSession2CommandGroup(Context context, + MediaSession2.CommandGroup instance, MediaSession2.CommandGroup others); + MediaSession2.CommandGroup fromBundle_MediaSession2CommandGroup(Context context, Bundle bundle); + ControllerInfoProvider createMediaSession2ControllerInfo(Context context, + MediaSession2.ControllerInfo instance, int uid, int pid, + String packageName, IInterface callback); + PlaylistParamsProvider createMediaSession2PlaylistParams(Context context, + PlaylistParams playlistParams, int repeatMode, int shuffleMode, + MediaMetadata2 playlistMetadata); + PlaylistParams fromBundle_PlaylistParams(Context context, Bundle bundle); + BuilderProvider createMediaSession2CommandButtonBuilder(Context context, Builder builder); + BuilderBaseProvider<MediaSession2, SessionCallback> createMediaSession2Builder( + Context context, MediaSession2.Builder instance, MediaPlayerInterface player); + + MediaController2Provider createMediaController2(Context context, MediaController2 instance, + SessionToken2 token, Executor executor, ControllerCallback callback); + + MediaBrowser2Provider createMediaBrowser2(Context context, MediaBrowser2 instance, + SessionToken2 token, Executor executor, BrowserCallback callback); + + MediaSessionService2Provider createMediaSessionService2(MediaSessionService2 instance); + MediaNotificationProvider createMediaSessionService2MediaNotification(Context context, + MediaNotification mediaNotification, int notificationId, Notification notification); + + MediaSessionService2Provider createMediaLibraryService2(MediaLibraryService2 instance); + BuilderBaseProvider<MediaLibrarySession, MediaLibrarySessionCallback> + createMediaLibraryService2Builder( + Context context, MediaLibrarySessionBuilder instance, MediaPlayerInterface player, + Executor callbackExecutor, MediaLibrarySessionCallback callback); + LibraryRootProvider createMediaLibraryService2LibraryRoot(Context context, LibraryRoot instance, + String rootId, Bundle extras); + + SessionToken2Provider createSessionToken2(Context context, SessionToken2 instance, + String packageName, String serviceName, int uid); + SessionToken2 SessionToken2_fromBundle(Context context, Bundle bundle); + + SessionPlayer2Provider createSessionPlayer2(Context context, SessionPlayer2 instance); + + MediaItem2Provider createMediaItem2(Context context, MediaItem2 mediaItem2, + String mediaId, DataSourceDesc dsd, MediaMetadata2 metadata, int flags); + MediaItem2 fromBundle_MediaItem2(Context context, Bundle bundle); + + VolumeProvider2Provider createVolumeProvider2(Context context, VolumeProvider2 instance, + int controlType, int maxVolume, int currentVolume); + + MediaMetadata2 fromBundle_MediaMetadata2(Context context, Bundle bundle); + MediaMetadata2Provider.BuilderProvider createMediaMetadata2Builder( + Context context, MediaMetadata2.Builder builder); + MediaMetadata2Provider.BuilderProvider createMediaMetadata2Builder( + Context context, MediaMetadata2.Builder builder, MediaMetadata2 source); + + Rating2 newUnratedRating_Rating2(Context context, int ratingStyle); + Rating2 fromBundle_Rating2(Context context, Bundle bundle); + Rating2 newHeartRating_Rating2(Context context, boolean hasHeart); + Rating2 newThumbRating_Rating2(Context context, boolean thumbIsUp); + Rating2 newStarRating_Rating2(Context context, int starRatingStyle, float starRating); + Rating2 newPercentageRating_Rating2(Context context, float percent); + + PlaybackState2Provider createPlaybackState2(Context context, PlaybackState2 instance, int state, + long position, long updateTime, float speed, long bufferedPosition, long activeItemId, + CharSequence error); + PlaybackState2 fromBundle_PlaybackState2(Context context, Bundle bundle); +} diff --git a/media/java/android/media/update/TransportControlProvider.java b/media/java/android/media/update/TransportControlProvider.java new file mode 100644 index 000000000000..44f82b295629 --- /dev/null +++ b/media/java/android/media/update/TransportControlProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 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.update; + +import android.media.PlaybackState2; + +/** + * @hide + */ +public interface TransportControlProvider { + void play_impl(); + void pause_impl(); + void stop_impl(); + void skipToPrevious_impl(); + void skipToNext_impl(); + + void prepare_impl(); + void fastForward_impl(); + void rewind_impl(); + void seekTo_impl(long pos); + void setCurrentPlaylistItem_impl(int index); + + PlaybackState2 getPlaybackState_impl(); +} diff --git a/media/java/android/media/update/VideoView2Provider.java b/media/java/android/media/update/VideoView2Provider.java new file mode 100644 index 000000000000..7f9ecdd5115c --- /dev/null +++ b/media/java/android/media/update/VideoView2Provider.java @@ -0,0 +1,81 @@ +/* + * Copyright 2018 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.update; + +import android.annotation.SystemApi; +import android.media.AudioAttributes; +import android.media.MediaPlayerInterface; +import android.media.session.MediaController; +import android.media.session.PlaybackState; +import android.media.session.MediaSession; +import android.net.Uri; +import android.util.AttributeSet; +import android.widget.MediaControlView2; +import android.widget.VideoView2; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Interface for connecting the public API to an updatable implementation. + * + * Each instance object is connected to one corresponding updatable object which implements the + * runtime behavior of that class. There should a corresponding provider method for all public + * methods. + * + * All methods behave as per their namesake in the public API. + * + * @see android.widget.VideoView2 + * + * @hide + */ +// TODO @SystemApi +public interface VideoView2Provider extends ViewGroupProvider { + void initialize(AttributeSet attrs, int defStyleAttr, int defStyleRes); + + void setMediaControlView2_impl(MediaControlView2 mediaControlView, long intervalMs); + MediaController getMediaController_impl(); + MediaControlView2 getMediaControlView2_impl(); + void setSubtitleEnabled_impl(boolean enable); + boolean isSubtitleEnabled_impl(); + // TODO: remove setSpeed_impl once MediaController2 is ready. + void setSpeed_impl(float speed); + void setAudioFocusRequest_impl(int focusGain); + void setAudioAttributes_impl(AudioAttributes attributes); + /** + * @hide + */ + void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerInterface player); + // TODO: remove setRouteAttributes_impl with MediaSession.Callback once MediaSession2 is ready. + void setRouteAttributes_impl(List<String> routeCategories, MediaSession.Callback sessionPlayer); + void setVideoPath_impl(String path); + void setVideoUri_impl(Uri uri); + void setVideoUri_impl(Uri uri, Map<String, String> headers); + void setViewType_impl(int viewType); + int getViewType_impl(); + void setCustomActions_impl(List<PlaybackState.CustomAction> actionList, + Executor executor, VideoView2.OnCustomActionListener listener); + void setOnPreparedListener_impl(Executor executor, VideoView2.OnPreparedListener l); + void setOnCompletionListener_impl(Executor executor, VideoView2.OnCompletionListener l); + void setOnErrorListener_impl(Executor executor, VideoView2.OnErrorListener l); + void setOnInfoListener_impl(Executor executor, VideoView2.OnInfoListener l); + void setOnViewTypeChangedListener_impl( + Executor executor, VideoView2.OnViewTypeChangedListener l); + void setFullScreenRequestListener_impl( + Executor executor, VideoView2.OnFullScreenRequestListener l); +} diff --git a/media/java/android/media/update/ViewGroupHelper.java b/media/java/android/media/update/ViewGroupHelper.java new file mode 100644 index 000000000000..6b4f15d0fdb7 --- /dev/null +++ b/media/java/android/media/update/ViewGroupHelper.java @@ -0,0 +1,369 @@ +/* + * Copyright 2018 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.update; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +/** + * Helper class for connecting the public API to an updatable implementation. + * + * @see ViewGroupProvider + * + * @hide + */ +public abstract class ViewGroupHelper<T extends ViewGroupProvider> extends ViewGroup { + /** @hide */ + final public T mProvider; + + /** @hide */ + public ViewGroupHelper(ProviderCreator<T> creator, + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + mProvider = creator.createProvider(this, new SuperProvider(), + new PrivateProvider()); + } + + /** @hide */ + // TODO @SystemApi + public T getProvider() { + return mProvider; + } + + @Override + protected void onAttachedToWindow() { + mProvider.onAttachedToWindow_impl(); + } + + @Override + protected void onDetachedFromWindow() { + mProvider.onDetachedFromWindow_impl(); + } + + @Override + public CharSequence getAccessibilityClassName() { + return mProvider.getAccessibilityClassName_impl(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mProvider.onTouchEvent_impl(ev); + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + return mProvider.onTrackballEvent_impl(ev); + } + + @Override + public void onFinishInflate() { + mProvider.onFinishInflate_impl(); + } + + @Override + public void setEnabled(boolean enabled) { + mProvider.setEnabled_impl(enabled); + } + + @Override + public void onVisibilityAggregated(boolean isVisible) { + mProvider.onVisibilityAggregated_impl(isVisible); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mProvider.onLayout_impl(changed, left, top, right, bottom); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mProvider.onMeasure_impl(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected int getSuggestedMinimumWidth() { + return mProvider.getSuggestedMinimumWidth_impl(); + } + + @Override + protected int getSuggestedMinimumHeight() { + return mProvider.getSuggestedMinimumHeight_impl(); + } + + // setMeasuredDimension is final + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return mProvider.dispatchTouchEvent_impl(ev); + } + + @Override + protected boolean checkLayoutParams(LayoutParams p) { + return mProvider.checkLayoutParams_impl(p); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return mProvider.generateDefaultLayoutParams_impl(); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return mProvider.generateLayoutParams_impl(attrs); + } + + @Override + protected LayoutParams generateLayoutParams(LayoutParams lp) { + return mProvider.generateLayoutParams_impl(lp); + } + + @Override + public boolean shouldDelayChildPressedState() { + return mProvider.shouldDelayChildPressedState_impl(); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + mProvider.measureChildWithMargins_impl(child, + parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed); + } + + /** @hide */ + public class SuperProvider implements ViewGroupProvider { + @Override + public CharSequence getAccessibilityClassName_impl() { + return ViewGroupHelper.super.getAccessibilityClassName(); + } + + @Override + public boolean onTouchEvent_impl(MotionEvent ev) { + return ViewGroupHelper.super.onTouchEvent(ev); + } + + @Override + public boolean onTrackballEvent_impl(MotionEvent ev) { + return ViewGroupHelper.super.onTrackballEvent(ev); + } + + @Override + public void onFinishInflate_impl() { + ViewGroupHelper.super.onFinishInflate(); + } + + @Override + public void setEnabled_impl(boolean enabled) { + ViewGroupHelper.super.setEnabled(enabled); + } + + @Override + public void onAttachedToWindow_impl() { + ViewGroupHelper.super.onAttachedToWindow(); + } + + @Override + public void onDetachedFromWindow_impl() { + ViewGroupHelper.super.onDetachedFromWindow(); + } + + @Override + public void onVisibilityAggregated_impl(boolean isVisible) { + ViewGroupHelper.super.onVisibilityAggregated(isVisible); + } + + @Override + public void onLayout_impl(boolean changed, int left, int top, int right, int bottom) { + // abstract method; no super + } + + @Override + public void onMeasure_impl(int widthMeasureSpec, int heightMeasureSpec) { + ViewGroupHelper.super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public int getSuggestedMinimumWidth_impl() { + return ViewGroupHelper.super.getSuggestedMinimumWidth(); + } + + @Override + public int getSuggestedMinimumHeight_impl() { + return ViewGroupHelper.super.getSuggestedMinimumHeight(); + } + + @Override + public void setMeasuredDimension_impl(int measuredWidth, int measuredHeight) { + ViewGroupHelper.super.setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + public boolean dispatchTouchEvent_impl(MotionEvent ev) { + return ViewGroupHelper.super.dispatchTouchEvent(ev); + } + + @Override + public boolean checkLayoutParams_impl(LayoutParams p) { + return ViewGroupHelper.super.checkLayoutParams(p); + } + + @Override + public LayoutParams generateDefaultLayoutParams_impl() { + return ViewGroupHelper.super.generateDefaultLayoutParams(); + } + + @Override + public LayoutParams generateLayoutParams_impl(AttributeSet attrs) { + return ViewGroupHelper.super.generateLayoutParams(attrs); + } + + @Override + public LayoutParams generateLayoutParams_impl(LayoutParams lp) { + return ViewGroupHelper.super.generateLayoutParams(lp); + } + + @Override + public boolean shouldDelayChildPressedState_impl() { + return ViewGroupHelper.super.shouldDelayChildPressedState(); + } + + @Override + public void measureChildWithMargins_impl(View child, + int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + ViewGroupHelper.super.measureChildWithMargins(child, + parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed); + } + } + + /** @hide */ + public class PrivateProvider implements ViewGroupProvider { + @Override + public CharSequence getAccessibilityClassName_impl() { + return ViewGroupHelper.this.getAccessibilityClassName(); + } + + @Override + public boolean onTouchEvent_impl(MotionEvent ev) { + return ViewGroupHelper.this.onTouchEvent(ev); + } + + @Override + public boolean onTrackballEvent_impl(MotionEvent ev) { + return ViewGroupHelper.this.onTrackballEvent(ev); + } + + @Override + public void onFinishInflate_impl() { + ViewGroupHelper.this.onFinishInflate(); + } + + @Override + public void setEnabled_impl(boolean enabled) { + ViewGroupHelper.this.setEnabled(enabled); + } + + @Override + public void onAttachedToWindow_impl() { + ViewGroupHelper.this.onAttachedToWindow(); + } + + @Override + public void onDetachedFromWindow_impl() { + ViewGroupHelper.this.onDetachedFromWindow(); + } + + @Override + public void onVisibilityAggregated_impl(boolean isVisible) { + ViewGroupHelper.this.onVisibilityAggregated(isVisible); + } + + @Override + public void onLayout_impl(boolean changed, int left, int top, int right, int bottom) { + ViewGroupHelper.this.onLayout(changed, left, top, right, bottom); + } + + @Override + public void onMeasure_impl(int widthMeasureSpec, int heightMeasureSpec) { + ViewGroupHelper.this.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public int getSuggestedMinimumWidth_impl() { + return ViewGroupHelper.this.getSuggestedMinimumWidth(); + } + + @Override + public int getSuggestedMinimumHeight_impl() { + return ViewGroupHelper.this.getSuggestedMinimumHeight(); + } + + @Override + public void setMeasuredDimension_impl(int measuredWidth, int measuredHeight) { + ViewGroupHelper.this.setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + public boolean dispatchTouchEvent_impl(MotionEvent ev) { + return ViewGroupHelper.this.dispatchTouchEvent(ev); + } + + @Override + public boolean checkLayoutParams_impl(LayoutParams p) { + return ViewGroupHelper.this.checkLayoutParams(p); + } + + @Override + public LayoutParams generateDefaultLayoutParams_impl() { + return ViewGroupHelper.this.generateDefaultLayoutParams(); + } + + @Override + public LayoutParams generateLayoutParams_impl(AttributeSet attrs) { + return ViewGroupHelper.this.generateLayoutParams(attrs); + } + + @Override + public LayoutParams generateLayoutParams_impl(LayoutParams lp) { + return ViewGroupHelper.this.generateLayoutParams(lp); + } + + @Override + public boolean shouldDelayChildPressedState_impl() { + return ViewGroupHelper.this.shouldDelayChildPressedState(); + } + + @Override + public void measureChildWithMargins_impl(View child, + int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + ViewGroupHelper.this.measureChildWithMargins(child, + parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed); + } + } + + /** @hide */ + @FunctionalInterface + public interface ProviderCreator<T extends ViewGroupProvider> { + T createProvider(ViewGroupHelper<T> instance, ViewGroupProvider superProvider, + ViewGroupProvider privateProvider); + } +} diff --git a/media/java/android/media/update/ViewGroupProvider.java b/media/java/android/media/update/ViewGroupProvider.java new file mode 100644 index 000000000000..67e8cea871e9 --- /dev/null +++ b/media/java/android/media/update/ViewGroupProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 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.update; + +import android.annotation.SystemApi; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup.LayoutParams; + +/** + * Interface for connecting the public API to an updatable implementation. + * + * Each instance object is connected to one corresponding updatable object which implements the + * runtime behavior of that class. There should a corresponding provider method for all public + * methods. + * + * All methods behave as per their namesake in the public API. + * + * @see android.view.View + * + * @hide + */ +// TODO @SystemApi +public interface ViewGroupProvider { + // View methods + void onAttachedToWindow_impl(); + void onDetachedFromWindow_impl(); + CharSequence getAccessibilityClassName_impl(); + boolean onTouchEvent_impl(MotionEvent ev); + boolean onTrackballEvent_impl(MotionEvent ev); + void onFinishInflate_impl(); + void setEnabled_impl(boolean enabled); + void onVisibilityAggregated_impl(boolean isVisible); + void onLayout_impl(boolean changed, int left, int top, int right, int bottom); + void onMeasure_impl(int widthMeasureSpec, int heightMeasureSpec); + int getSuggestedMinimumWidth_impl(); + int getSuggestedMinimumHeight_impl(); + void setMeasuredDimension_impl(int measuredWidth, int measuredHeight); + boolean dispatchTouchEvent_impl(MotionEvent ev); + + // ViewGroup methods + boolean checkLayoutParams_impl(LayoutParams p); + LayoutParams generateDefaultLayoutParams_impl(); + LayoutParams generateLayoutParams_impl(AttributeSet attrs); + LayoutParams generateLayoutParams_impl(LayoutParams lp); + boolean shouldDelayChildPressedState_impl(); + void measureChildWithMargins_impl(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed); + + // ViewManager methods + // ViewParent methods +} diff --git a/media/java/android/media/update/VolumeProvider2Provider.java b/media/java/android/media/update/VolumeProvider2Provider.java new file mode 100644 index 000000000000..5657af60eb6b --- /dev/null +++ b/media/java/android/media/update/VolumeProvider2Provider.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 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.update; + +/** + * @hide + */ +// TODO(jaewan): @SystemApi +public interface VolumeProvider2Provider { + int getControlType_impl(); + int getMaxVolume_impl(); + int getCurrentVolume_impl(); + void setCurrentVolume_impl(int currentVolume); +} diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java index ba29d2daaa0e..a647dcc2d4b9 100755 --- a/media/java/android/mtp/MtpDatabase.java +++ b/media/java/android/mtp/MtpDatabase.java @@ -30,6 +30,7 @@ import android.net.Uri; import android.os.BatteryManager; import android.os.RemoteException; import android.os.SystemProperties; +import android.os.storage.StorageVolume; import android.provider.MediaStore; import android.provider.MediaStore.Audio; import android.provider.MediaStore.Files; @@ -40,21 +41,31 @@ import android.view.WindowManager; import dalvik.system.CloseGuard; +import com.google.android.collect.Sets; + import java.io.File; -import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.IntStream; +import java.util.stream.Stream; /** + * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses + * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File + * operations are also reflected in MediaProvider if possible. + * operations * {@hide} */ public class MtpDatabase implements AutoCloseable { - private static final String TAG = "MtpDatabase"; + private static final String TAG = MtpDatabase.class.getSimpleName(); - private final Context mUserContext; private final Context mContext; - private final String mPackageName; private final ContentProviderClient mMediaProvider; private final String mVolumeName; private final Uri mObjectsUri; @@ -63,86 +74,158 @@ public class MtpDatabase implements AutoCloseable { private final AtomicBoolean mClosed = new AtomicBoolean(); private final CloseGuard mCloseGuard = CloseGuard.get(); - // path to primary storage - private final String mMediaStoragePath; - // if not null, restrict all queries to these subdirectories - private final String[] mSubDirectories; - // where clause for restricting queries to files in mSubDirectories - private String mSubDirectoriesWhere; - // where arguments for restricting queries to files in mSubDirectories - private String[] mSubDirectoriesWhereArgs; - - private final HashMap<String, MtpStorage> mStorageMap = new HashMap<String, MtpStorage>(); + private final HashMap<String, MtpStorage> mStorageMap = new HashMap<>(); // cached property groups for single properties - private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty - = new HashMap<Integer, MtpPropertyGroup>(); + private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty = new HashMap<>(); // cached property groups for all properties for a given format - private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat - = new HashMap<Integer, MtpPropertyGroup>(); - - // true if the database has been modified in the current MTP session - private boolean mDatabaseModified; + private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat = new HashMap<>(); // SharedPreferences for writable MTP device properties private SharedPreferences mDeviceProperties; - private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1; - private static final String[] ID_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 + // Cached device properties + private int mBatteryLevel; + private int mBatteryScale; + private int mDeviceType; + + private MtpServer mServer; + private MtpStorageManager mManager; + + private static final String PATH_WHERE = Files.FileColumns.DATA + "=?"; + private static final String[] ID_PROJECTION = new String[] {Files.FileColumns._ID}; + private static final String[] PATH_PROJECTION = new String[] {Files.FileColumns.DATA}; + private static final String NO_MEDIA = ".nomedia"; + + static { + System.loadLibrary("media_jni"); + } + + private static final int[] PLAYBACK_FORMATS = { + // allow transferring arbitrary files + MtpConstants.FORMAT_UNDEFINED, + + MtpConstants.FORMAT_ASSOCIATION, + MtpConstants.FORMAT_TEXT, + MtpConstants.FORMAT_HTML, + MtpConstants.FORMAT_WAV, + MtpConstants.FORMAT_MP3, + MtpConstants.FORMAT_MPEG, + MtpConstants.FORMAT_EXIF_JPEG, + MtpConstants.FORMAT_TIFF_EP, + MtpConstants.FORMAT_BMP, + MtpConstants.FORMAT_GIF, + MtpConstants.FORMAT_JFIF, + MtpConstants.FORMAT_PNG, + MtpConstants.FORMAT_TIFF, + MtpConstants.FORMAT_WMA, + MtpConstants.FORMAT_OGG, + MtpConstants.FORMAT_AAC, + MtpConstants.FORMAT_MP4_CONTAINER, + MtpConstants.FORMAT_MP2, + MtpConstants.FORMAT_3GP_CONTAINER, + MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST, + MtpConstants.FORMAT_WPL_PLAYLIST, + MtpConstants.FORMAT_M3U_PLAYLIST, + MtpConstants.FORMAT_PLS_PLAYLIST, + MtpConstants.FORMAT_XML_DOCUMENT, + MtpConstants.FORMAT_FLAC, + MtpConstants.FORMAT_DNG, + MtpConstants.FORMAT_HEIF, }; - private static final String[] PATH_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.DATA, // 1 + + private static final int[] FILE_PROPERTIES = { + MtpConstants.PROPERTY_STORAGE_ID, + MtpConstants.PROPERTY_OBJECT_FORMAT, + MtpConstants.PROPERTY_PROTECTION_STATUS, + MtpConstants.PROPERTY_OBJECT_SIZE, + MtpConstants.PROPERTY_OBJECT_FILE_NAME, + MtpConstants.PROPERTY_DATE_MODIFIED, + MtpConstants.PROPERTY_PERSISTENT_UID, + MtpConstants.PROPERTY_PARENT_OBJECT, + MtpConstants.PROPERTY_NAME, + MtpConstants.PROPERTY_DISPLAY_NAME, + MtpConstants.PROPERTY_DATE_ADDED, }; - private static final String[] FORMAT_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.FORMAT, // 1 + + private static final int[] AUDIO_PROPERTIES = { + MtpConstants.PROPERTY_ARTIST, + MtpConstants.PROPERTY_ALBUM_NAME, + MtpConstants.PROPERTY_ALBUM_ARTIST, + MtpConstants.PROPERTY_TRACK, + MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE, + MtpConstants.PROPERTY_DURATION, + MtpConstants.PROPERTY_GENRE, + MtpConstants.PROPERTY_COMPOSER, + MtpConstants.PROPERTY_AUDIO_WAVE_CODEC, + MtpConstants.PROPERTY_BITRATE_TYPE, + MtpConstants.PROPERTY_AUDIO_BITRATE, + MtpConstants.PROPERTY_NUMBER_OF_CHANNELS, + MtpConstants.PROPERTY_SAMPLE_RATE, }; - private static final String[] PATH_FORMAT_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.DATA, // 1 - Files.FileColumns.FORMAT, // 2 + + private static final int[] VIDEO_PROPERTIES = { + MtpConstants.PROPERTY_ARTIST, + MtpConstants.PROPERTY_ALBUM_NAME, + MtpConstants.PROPERTY_DURATION, + MtpConstants.PROPERTY_DESCRIPTION, }; - private static final String[] OBJECT_INFO_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.STORAGE_ID, // 1 - Files.FileColumns.FORMAT, // 2 - Files.FileColumns.PARENT, // 3 - Files.FileColumns.DATA, // 4 - Files.FileColumns.DATE_ADDED, // 5 - Files.FileColumns.DATE_MODIFIED, // 6 + + private static final int[] IMAGE_PROPERTIES = { + MtpConstants.PROPERTY_DESCRIPTION, }; - private static final String ID_WHERE = Files.FileColumns._ID + "=?"; - private static final String PATH_WHERE = Files.FileColumns.DATA + "=?"; - private static final String STORAGE_WHERE = Files.FileColumns.STORAGE_ID + "=?"; - private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?"; - private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?"; - private static final String STORAGE_FORMAT_WHERE = STORAGE_WHERE + " AND " - + Files.FileColumns.FORMAT + "=?"; - private static final String STORAGE_PARENT_WHERE = STORAGE_WHERE + " AND " - + Files.FileColumns.PARENT + "=?"; - private static final String FORMAT_PARENT_WHERE = FORMAT_WHERE + " AND " - + Files.FileColumns.PARENT + "=?"; - private static final String STORAGE_FORMAT_PARENT_WHERE = STORAGE_FORMAT_WHERE + " AND " - + Files.FileColumns.PARENT + "=?"; + private static final int[] DEVICE_PROPERTIES = { + MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER, + MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME, + MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE, + MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL, + MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE, + }; - private MtpServer mServer; + private int[] getSupportedObjectProperties(int format) { + switch (format) { + case MtpConstants.FORMAT_MP3: + case MtpConstants.FORMAT_WAV: + case MtpConstants.FORMAT_WMA: + case MtpConstants.FORMAT_OGG: + case MtpConstants.FORMAT_AAC: + return IntStream.concat(Arrays.stream(FILE_PROPERTIES), + Arrays.stream(AUDIO_PROPERTIES)).toArray(); + case MtpConstants.FORMAT_MPEG: + case MtpConstants.FORMAT_3GP_CONTAINER: + case MtpConstants.FORMAT_WMV: + return IntStream.concat(Arrays.stream(FILE_PROPERTIES), + Arrays.stream(VIDEO_PROPERTIES)).toArray(); + case MtpConstants.FORMAT_EXIF_JPEG: + case MtpConstants.FORMAT_GIF: + case MtpConstants.FORMAT_PNG: + case MtpConstants.FORMAT_BMP: + case MtpConstants.FORMAT_DNG: + case MtpConstants.FORMAT_HEIF: + return IntStream.concat(Arrays.stream(FILE_PROPERTIES), + Arrays.stream(IMAGE_PROPERTIES)).toArray(); + default: + return FILE_PROPERTIES; + } + } - // read from native code - private int mBatteryLevel; - private int mBatteryScale; + private int[] getSupportedDeviceProperties() { + return DEVICE_PROPERTIES; + } - private int mDeviceType; + private int[] getSupportedPlaybackFormats() { + return PLAYBACK_FORMATS; + } - static { - System.loadLibrary("media_jni"); + private int[] getSupportedCaptureFormats() { + // no capture formats yet + return null; } private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() { - @Override + @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { @@ -160,61 +243,42 @@ public class MtpDatabase implements AutoCloseable { } }; - public MtpDatabase(Context context, Context userContext, String volumeName, String storagePath, + public MtpDatabase(Context context, Context userContext, String volumeName, String[] subDirectories) { native_setup(); - mContext = context; - mUserContext = userContext; - mPackageName = context.getPackageName(); mMediaProvider = userContext.getContentResolver() .acquireContentProviderClient(MediaStore.AUTHORITY); mVolumeName = volumeName; - mMediaStoragePath = storagePath; mObjectsUri = Files.getMtpObjectsUri(volumeName); mMediaScanner = new MediaScanner(context, mVolumeName); - - mSubDirectories = subDirectories; - if (subDirectories != null) { - // Compute "where" string for restricting queries to subdirectories - StringBuilder builder = new StringBuilder(); - builder.append("("); - int count = subDirectories.length; - for (int i = 0; i < count; i++) { - builder.append(Files.FileColumns.DATA + "=? OR " - + Files.FileColumns.DATA + " LIKE ?"); - if (i != count - 1) { - builder.append(" OR "); - } + mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() { + @Override + public void sendObjectAdded(int id) { + if (MtpDatabase.this.mServer != null) + MtpDatabase.this.mServer.sendObjectAdded(id); } - builder.append(")"); - mSubDirectoriesWhere = builder.toString(); - - // Compute "where" arguments for restricting queries to subdirectories - mSubDirectoriesWhereArgs = new String[count * 2]; - for (int i = 0, j = 0; i < count; i++) { - String path = subDirectories[i]; - mSubDirectoriesWhereArgs[j++] = path; - mSubDirectoriesWhereArgs[j++] = path + "/%"; + + @Override + public void sendObjectRemoved(int id) { + if (MtpDatabase.this.mServer != null) + MtpDatabase.this.mServer.sendObjectRemoved(id); } - } + }, subDirectories == null ? null : Sets.newHashSet(subDirectories)); initDeviceProperties(context); mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0); - mCloseGuard.open("close"); } public void setServer(MtpServer server) { mServer = server; - // always unregister before registering try { mContext.unregisterReceiver(mBatteryReceiver); } catch (IllegalArgumentException e) { // wasn't previously registered, ignore } - // register for battery notifications when we are connected if (server != null) { mContext.registerReceiver(mBatteryReceiver, @@ -224,6 +288,7 @@ public class MtpDatabase implements AutoCloseable { @Override public void close() { + mManager.close(); mCloseGuard.close(); if (mClosed.compareAndSet(false, true)) { mMediaScanner.close(); @@ -238,24 +303,32 @@ public class MtpDatabase implements AutoCloseable { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } - close(); } finally { super.finalize(); } } - public void addStorage(MtpStorage storage) { - mStorageMap.put(storage.getPath(), storage); + public void addStorage(StorageVolume storage) { + MtpStorage mtpStorage = mManager.addMtpStorage(storage); + mStorageMap.put(storage.getPath(), mtpStorage); + mServer.addStorage(mtpStorage); } - public void removeStorage(MtpStorage storage) { + public void removeStorage(StorageVolume storage) { + MtpStorage mtpStorage = mStorageMap.get(storage.getPath()); + if (mtpStorage == null) { + return; + } + mServer.removeStorage(mtpStorage); + mManager.removeMtpStorage(mtpStorage); mStorageMap.remove(storage.getPath()); } private void initDeviceProperties(Context context) { final String devicePropertiesName = "device-properties"; - mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE); + mDeviceProperties = context.getSharedPreferences(devicePropertiesName, + Context.MODE_PRIVATE); File databaseFile = context.getDatabasePath(devicePropertiesName); if (databaseFile.exists()) { @@ -266,7 +339,7 @@ public class MtpDatabase implements AutoCloseable { try { db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null); if (db != null) { - c = db.query("properties", new String[] { "_id", "code", "value" }, + c = db.query("properties", new String[]{"_id", "code", "value"}, null, null, null, null, null); if (c != null) { SharedPreferences.Editor e = mDeviceProperties.edit(); @@ -288,608 +361,371 @@ public class MtpDatabase implements AutoCloseable { } } - // check to see if the path is contained in one of our storage subdirectories - // returns true if we have no special subdirectories - private boolean inStorageSubDirectory(String path) { - if (mSubDirectories == null) return true; - if (path == null) return false; - - boolean allowed = false; - int pathLength = path.length(); - for (int i = 0; i < mSubDirectories.length && !allowed; i++) { - String subdir = mSubDirectories[i]; - int subdirLength = subdir.length(); - if (subdirLength < pathLength && - path.charAt(subdirLength) == '/' && - path.startsWith(subdir)) { - allowed = true; - } - } - return allowed; - } - - // check to see if the path matches one of our storage subdirectories - // returns true if we have no special subdirectories - private boolean isStorageSubDirectory(String path) { - if (mSubDirectories == null) return false; - for (int i = 0; i < mSubDirectories.length; i++) { - if (path.equals(mSubDirectories[i])) { - return true; - } + private int beginSendObject(String path, int format, int parent, int storageId) { + MtpStorageManager.MtpObject parentObj = + parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent); + if (parentObj == null) { + return -1; } - return false; - } - // returns true if the path is in the storage root - private boolean inStorageRoot(String path) { - try { - File f = new File(path); - String canonical = f.getCanonicalPath(); - for (String root: mStorageMap.keySet()) { - if (canonical.startsWith(root)) { - return true; - } - } - } catch (IOException e) { - // ignore - } - return false; + Path objPath = Paths.get(path); + return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format); } - private int beginSendObject(String path, int format, int parent, - int storageId, long size, long modified) { - // if the path is outside of the storage root, do not allow access - if (!inStorageRoot(path)) { - Log.e(TAG, "attempt to put file outside of storage area: " + path); - return -1; + private void endSendObject(int handle, boolean succeeded) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null || !mManager.endSendObject(obj, succeeded)) { + Log.e(TAG, "Failed to successfully end send object"); + return; } - // if mSubDirectories is not null, do not allow copying files to any other locations - if (!inStorageSubDirectory(path)) return -1; - - // make sure the object does not exist - if (path != null) { - Cursor c = null; + // Add the new file to MediaProvider + if (succeeded) { + String path = obj.getPath().toString(); + int format = obj.getFormat(); + // Get parent info from MediaProvider, since the id is different from MTP's + ContentValues values = new ContentValues(); + values.put(Files.FileColumns.DATA, path); + values.put(Files.FileColumns.FORMAT, format); + values.put(Files.FileColumns.SIZE, obj.getSize()); + values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime()); try { - c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE, - new String[] { path }, null, null); - if (c != null && c.getCount() > 0) { - Log.w(TAG, "file already exists in beginSendObject: " + path); - return -1; + if (obj.getParent().isRoot()) { + values.put(Files.FileColumns.PARENT, 0); + } else { + int parentId = findInMedia(obj.getParent().getPath()); + if (parentId != -1) { + values.put(Files.FileColumns.PARENT, parentId); + } else { + // The parent isn't in MediaProvider. Don't add the new file. + return; + } + } + + Uri uri = mMediaProvider.insert(mObjectsUri, values); + if (uri != null) { + rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format); } } catch (RemoteException e) { Log.e(TAG, "RemoteException in beginSendObject", e); - } finally { - if (c != null) { - c.close(); - } - } - } - - mDatabaseModified = true; - ContentValues values = new ContentValues(); - values.put(Files.FileColumns.DATA, path); - values.put(Files.FileColumns.FORMAT, format); - values.put(Files.FileColumns.PARENT, parent); - values.put(Files.FileColumns.STORAGE_ID, storageId); - values.put(Files.FileColumns.SIZE, size); - values.put(Files.FileColumns.DATE_MODIFIED, modified); - - try { - Uri uri = mMediaProvider.insert(mObjectsUri, values); - if (uri != null) { - return Integer.parseInt(uri.getPathSegments().get(2)); - } else { - return -1; } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in beginSendObject", e); - return -1; } } - private void endSendObject(String path, int handle, int format, boolean succeeded) { - if (succeeded) { - // handle abstract playlists separately - // they do not exist in the file system so don't use the media scanner here - if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) { - // extract name from path - String name = path; - int lastSlash = name.lastIndexOf('/'); - if (lastSlash >= 0) { - name = name.substring(lastSlash + 1); - } - // strip trailing ".pla" from the name - if (name.endsWith(".pla")) { - name = name.substring(0, name.length() - 4); - } + private void rescanFile(String path, int handle, int format) { + // handle abstract playlists separately + // they do not exist in the file system so don't use the media scanner here + if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) { + // extract name from path + String name = path; + int lastSlash = name.lastIndexOf('/'); + if (lastSlash >= 0) { + name = name.substring(lastSlash + 1); + } + // strip trailing ".pla" from the name + if (name.endsWith(".pla")) { + name = name.substring(0, name.length() - 4); + } - ContentValues values = new ContentValues(1); - values.put(Audio.Playlists.DATA, path); - values.put(Audio.Playlists.NAME, name); - values.put(Files.FileColumns.FORMAT, format); - values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000); - values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle); - try { - Uri uri = mMediaProvider.insert( - Audio.Playlists.EXTERNAL_CONTENT_URI, values); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in endSendObject", e); - } - } else { - mMediaScanner.scanMtpFile(path, handle, format); + ContentValues values = new ContentValues(1); + values.put(Audio.Playlists.DATA, path); + values.put(Audio.Playlists.NAME, name); + values.put(Files.FileColumns.FORMAT, format); + values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000); + values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle); + try { + mMediaProvider.insert( + Audio.Playlists.EXTERNAL_CONTENT_URI, values); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in endSendObject", e); } } else { - deleteFile(handle); + mMediaScanner.scanMtpFile(path, handle, format); } } - private void doScanDirectory(String path) { - String[] scanPath; - scanPath = new String[] { path }; - mMediaScanner.scanDirectories(scanPath); + private int[] getObjectList(int storageID, int format, int parent) { + Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent, + format, storageID); + if (objectStream == null) { + return null; + } + return objectStream.mapToInt(MtpStorageManager.MtpObject::getId).toArray(); } - private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException { - String where; - String[] whereArgs; - - if (storageID == 0xFFFFFFFF) { - // query all stores - if (format == 0) { - // query all formats - if (parent == 0) { - // query all objects - where = null; - whereArgs = null; - } else { - if (parent == 0xFFFFFFFF) { - // all objects in root of store - parent = 0; - } - where = PARENT_WHERE; - whereArgs = new String[] { Integer.toString(parent) }; - } - } else { - // query specific format - if (parent == 0) { - // query all objects - where = FORMAT_WHERE; - whereArgs = new String[] { Integer.toString(format) }; - } else { - if (parent == 0xFFFFFFFF) { - // all objects in root of store - parent = 0; - } - where = FORMAT_PARENT_WHERE; - whereArgs = new String[] { Integer.toString(format), - Integer.toString(parent) }; - } - } - } else { - // query specific store - if (format == 0) { - // query all formats - if (parent == 0) { - // query all objects - where = STORAGE_WHERE; - whereArgs = new String[] { Integer.toString(storageID) }; - } else { - if (parent == 0xFFFFFFFF) { - // all objects in root of store - parent = 0; - where = STORAGE_PARENT_WHERE; - whereArgs = new String[]{Integer.toString(storageID), - Integer.toString(parent)}; - } else { - // If a parent is specified, the storage is redundant - where = PARENT_WHERE; - whereArgs = new String[]{Integer.toString(parent)}; - } - } - } else { - // query specific format - if (parent == 0) { - // query all objects - where = STORAGE_FORMAT_WHERE; - whereArgs = new String[] { Integer.toString(storageID), - Integer.toString(format) }; - } else { - if (parent == 0xFFFFFFFF) { - // all objects in root of store - parent = 0; - where = STORAGE_FORMAT_PARENT_WHERE; - whereArgs = new String[]{Integer.toString(storageID), - Integer.toString(format), - Integer.toString(parent)}; - } else { - // If a parent is specified, the storage is redundant - where = FORMAT_PARENT_WHERE; - whereArgs = new String[]{Integer.toString(format), - Integer.toString(parent)}; - } - } - } + private int getNumObjects(int storageID, int format, int parent) { + Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent, + format, storageID); + if (objectStream == null) { + return -1; } + return (int) objectStream.count(); + } - // if we are restricting queries to mSubDirectories, we need to add the restriction - // onto our "where" arguments - if (mSubDirectoriesWhere != null) { - if (where == null) { - where = mSubDirectoriesWhere; - whereArgs = mSubDirectoriesWhereArgs; - } else { - where = where + " AND " + mSubDirectoriesWhere; - - // create new array to hold whereArgs and mSubDirectoriesWhereArgs - String[] newWhereArgs = - new String[whereArgs.length + mSubDirectoriesWhereArgs.length]; - int i, j; - for (i = 0; i < whereArgs.length; i++) { - newWhereArgs[i] = whereArgs[i]; - } - for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) { - newWhereArgs[i] = mSubDirectoriesWhereArgs[j]; - } - whereArgs = newWhereArgs; + private MtpPropertyList getObjectPropertyList(int handle, int format, int property, + int groupCode, int depth) { + // FIXME - implement group support + if (property == 0) { + if (groupCode == 0) { + return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED); } + return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED); } - - return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where, - whereArgs, null, null); - } - - private int[] getObjectList(int storageID, int format, int parent) { - Cursor c = null; - try { - c = createObjectQuery(storageID, format, parent); - if (c == null) { - return null; + if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) { + // request all objects starting at root + handle = 0xFFFFFFFF; + depth = 0; + } + if (!(depth == 0 || depth == 1)) { + // we only support depth 0 and 1 + // depth 0: single object, depth 1: immediate children + return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED); + } + Stream<MtpStorageManager.MtpObject> objectStream = Stream.of(); + if (handle == 0xFFFFFFFF) { + // All objects are requested + objectStream = mManager.getObjects(0, format, 0xFFFFFFFF); + if (objectStream == null) { + return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } - int count = c.getCount(); - if (count > 0) { - int[] result = new int[count]; - for (int i = 0; i < count; i++) { - c.moveToNext(); - result[i] = c.getInt(0); - } - return result; + } else if (handle != 0) { + // Add the requested object if format matches + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectList", e); - } finally { - if (c != null) { - c.close(); + if (obj.getFormat() == format || format == 0) { + objectStream = Stream.of(obj); } } - return null; - } - - private int getNumObjects(int storageID, int format, int parent) { - Cursor c = null; - try { - c = createObjectQuery(storageID, format, parent); - if (c != null) { - return c.getCount(); + if (handle == 0 || depth == 1) { + if (handle == 0) { + handle = 0xFFFFFFFF; } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getNumObjects", e); - } finally { - if (c != null) { - c.close(); + // Get the direct children of root or this object. + Stream<MtpStorageManager.MtpObject> childStream = mManager.getObjects(handle, format, + 0xFFFFFFFF); + if (childStream == null) { + return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } - } - return -1; - } - - private int[] getSupportedPlaybackFormats() { - return new int[] { - // allow transfering arbitrary files - MtpConstants.FORMAT_UNDEFINED, - - MtpConstants.FORMAT_ASSOCIATION, - MtpConstants.FORMAT_TEXT, - MtpConstants.FORMAT_HTML, - MtpConstants.FORMAT_WAV, - MtpConstants.FORMAT_MP3, - MtpConstants.FORMAT_MPEG, - MtpConstants.FORMAT_EXIF_JPEG, - MtpConstants.FORMAT_TIFF_EP, - MtpConstants.FORMAT_BMP, - MtpConstants.FORMAT_GIF, - MtpConstants.FORMAT_JFIF, - MtpConstants.FORMAT_PNG, - MtpConstants.FORMAT_TIFF, - MtpConstants.FORMAT_WMA, - MtpConstants.FORMAT_OGG, - MtpConstants.FORMAT_AAC, - MtpConstants.FORMAT_MP4_CONTAINER, - MtpConstants.FORMAT_MP2, - MtpConstants.FORMAT_3GP_CONTAINER, - MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST, - MtpConstants.FORMAT_WPL_PLAYLIST, - MtpConstants.FORMAT_M3U_PLAYLIST, - MtpConstants.FORMAT_PLS_PLAYLIST, - MtpConstants.FORMAT_XML_DOCUMENT, - MtpConstants.FORMAT_FLAC, - MtpConstants.FORMAT_DNG, - MtpConstants.FORMAT_HEIF, - }; - } - - private int[] getSupportedCaptureFormats() { - // no capture formats yet - return null; - } - - static final int[] FILE_PROPERTIES = { - // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES - // and IMAGE_PROPERTIES below - MtpConstants.PROPERTY_STORAGE_ID, - MtpConstants.PROPERTY_OBJECT_FORMAT, - MtpConstants.PROPERTY_PROTECTION_STATUS, - MtpConstants.PROPERTY_OBJECT_SIZE, - MtpConstants.PROPERTY_OBJECT_FILE_NAME, - MtpConstants.PROPERTY_DATE_MODIFIED, - MtpConstants.PROPERTY_PARENT_OBJECT, - MtpConstants.PROPERTY_PERSISTENT_UID, - MtpConstants.PROPERTY_NAME, - MtpConstants.PROPERTY_DISPLAY_NAME, - MtpConstants.PROPERTY_DATE_ADDED, - }; - - static final int[] AUDIO_PROPERTIES = { - // NOTE must match FILE_PROPERTIES above - MtpConstants.PROPERTY_STORAGE_ID, - MtpConstants.PROPERTY_OBJECT_FORMAT, - MtpConstants.PROPERTY_PROTECTION_STATUS, - MtpConstants.PROPERTY_OBJECT_SIZE, - MtpConstants.PROPERTY_OBJECT_FILE_NAME, - MtpConstants.PROPERTY_DATE_MODIFIED, - MtpConstants.PROPERTY_PARENT_OBJECT, - MtpConstants.PROPERTY_PERSISTENT_UID, - MtpConstants.PROPERTY_NAME, - MtpConstants.PROPERTY_DISPLAY_NAME, - MtpConstants.PROPERTY_DATE_ADDED, - - // audio specific properties - MtpConstants.PROPERTY_ARTIST, - MtpConstants.PROPERTY_ALBUM_NAME, - MtpConstants.PROPERTY_ALBUM_ARTIST, - MtpConstants.PROPERTY_TRACK, - MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE, - MtpConstants.PROPERTY_DURATION, - MtpConstants.PROPERTY_GENRE, - MtpConstants.PROPERTY_COMPOSER, - MtpConstants.PROPERTY_AUDIO_WAVE_CODEC, - MtpConstants.PROPERTY_BITRATE_TYPE, - MtpConstants.PROPERTY_AUDIO_BITRATE, - MtpConstants.PROPERTY_NUMBER_OF_CHANNELS, - MtpConstants.PROPERTY_SAMPLE_RATE, - }; - - static final int[] VIDEO_PROPERTIES = { - // NOTE must match FILE_PROPERTIES above - MtpConstants.PROPERTY_STORAGE_ID, - MtpConstants.PROPERTY_OBJECT_FORMAT, - MtpConstants.PROPERTY_PROTECTION_STATUS, - MtpConstants.PROPERTY_OBJECT_SIZE, - MtpConstants.PROPERTY_OBJECT_FILE_NAME, - MtpConstants.PROPERTY_DATE_MODIFIED, - MtpConstants.PROPERTY_PARENT_OBJECT, - MtpConstants.PROPERTY_PERSISTENT_UID, - MtpConstants.PROPERTY_NAME, - MtpConstants.PROPERTY_DISPLAY_NAME, - MtpConstants.PROPERTY_DATE_ADDED, - - // video specific properties - MtpConstants.PROPERTY_ARTIST, - MtpConstants.PROPERTY_ALBUM_NAME, - MtpConstants.PROPERTY_DURATION, - MtpConstants.PROPERTY_DESCRIPTION, - }; - - static final int[] IMAGE_PROPERTIES = { - // NOTE must match FILE_PROPERTIES above - MtpConstants.PROPERTY_STORAGE_ID, - MtpConstants.PROPERTY_OBJECT_FORMAT, - MtpConstants.PROPERTY_PROTECTION_STATUS, - MtpConstants.PROPERTY_OBJECT_SIZE, - MtpConstants.PROPERTY_OBJECT_FILE_NAME, - MtpConstants.PROPERTY_DATE_MODIFIED, - MtpConstants.PROPERTY_PARENT_OBJECT, - MtpConstants.PROPERTY_PERSISTENT_UID, - MtpConstants.PROPERTY_NAME, - MtpConstants.PROPERTY_DISPLAY_NAME, - MtpConstants.PROPERTY_DATE_ADDED, - - // image specific properties - MtpConstants.PROPERTY_DESCRIPTION, - }; - - private int[] getSupportedObjectProperties(int format) { - switch (format) { - case MtpConstants.FORMAT_MP3: - case MtpConstants.FORMAT_WAV: - case MtpConstants.FORMAT_WMA: - case MtpConstants.FORMAT_OGG: - case MtpConstants.FORMAT_AAC: - return AUDIO_PROPERTIES; - case MtpConstants.FORMAT_MPEG: - case MtpConstants.FORMAT_3GP_CONTAINER: - case MtpConstants.FORMAT_WMV: - return VIDEO_PROPERTIES; - case MtpConstants.FORMAT_EXIF_JPEG: - case MtpConstants.FORMAT_GIF: - case MtpConstants.FORMAT_PNG: - case MtpConstants.FORMAT_BMP: - case MtpConstants.FORMAT_DNG: - case MtpConstants.FORMAT_HEIF: - return IMAGE_PROPERTIES; - default: - return FILE_PROPERTIES; - } - } - - private int[] getSupportedDeviceProperties() { - return new int[] { - MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER, - MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME, - MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE, - MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL, - MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE, - }; - } - - private MtpPropertyList getObjectPropertyList(int handle, int format, int property, - int groupCode, int depth) { - // FIXME - implement group support - if (groupCode != 0) { - return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED); + objectStream = Stream.concat(objectStream, childStream); } + MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK); MtpPropertyGroup propertyGroup; - if (property == 0xffffffff) { - if (format == 0 && handle != 0 && handle != 0xffffffff) { - // return properties based on the object's format - format = getObjectFormat(handle); - } - propertyGroup = mPropertyGroupsByFormat.get(format); - if (propertyGroup == null) { - int[] propertyList = getSupportedObjectProperties(format); - propertyGroup = new MtpPropertyGroup(this, mMediaProvider, - mVolumeName, propertyList); - mPropertyGroupsByFormat.put(format, propertyGroup); + Iterator<MtpStorageManager.MtpObject> iter = objectStream.iterator(); + while (iter.hasNext()) { + MtpStorageManager.MtpObject obj = iter.next(); + if (property == 0xffffffff) { + // Get all properties supported by this object + propertyGroup = mPropertyGroupsByFormat.get(obj.getFormat()); + if (propertyGroup == null) { + int[] propertyList = getSupportedObjectProperties(format); + propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName, + propertyList); + mPropertyGroupsByFormat.put(format, propertyGroup); + } + } else { + // Get this property value + final int[] propertyList = new int[]{property}; + propertyGroup = mPropertyGroupsByProperty.get(property); + if (propertyGroup == null) { + propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName, + propertyList); + mPropertyGroupsByProperty.put(property, propertyGroup); + } } - } else { - propertyGroup = mPropertyGroupsByProperty.get(property); - if (propertyGroup == null) { - final int[] propertyList = new int[] { property }; - propertyGroup = new MtpPropertyGroup( - this, mMediaProvider, mVolumeName, propertyList); - mPropertyGroupsByProperty.put(property, propertyGroup); + int err = propertyGroup.getPropertyList(obj, ret); + if (err != MtpConstants.RESPONSE_OK) { + return new MtpPropertyList(err); } } - - return propertyGroup.getPropertyList(handle, format, depth); + return ret; } private int renameFile(int handle, String newName) { - Cursor c = null; - - // first compute current path - String path = null; - String[] whereArgs = new String[] { Integer.toString(handle) }; - try { - c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, - whereArgs, null, null); - if (c != null && c.moveToNext()) { - path = c.getString(1); - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectFilePath", e); - return MtpConstants.RESPONSE_GENERAL_ERROR; - } finally { - if (c != null) { - c.close(); - } - } - if (path == null) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; } - - // do not allow renaming any of the special subdirectories - if (isStorageSubDirectory(path)) { - return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; - } + Path oldPath = obj.getPath(); // now rename the file. make sure this succeeds before updating database - File oldFile = new File(path); - int lastSlash = path.lastIndexOf('/'); - if (lastSlash <= 1) { + if (!mManager.beginRenameObject(obj, newName)) return MtpConstants.RESPONSE_GENERAL_ERROR; + Path newPath = obj.getPath(); + boolean success = oldPath.toFile().renameTo(newPath.toFile()); + if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) { + Log.e(TAG, "Failed to end rename object"); } - String newPath = path.substring(0, lastSlash + 1) + newName; - File newFile = new File(newPath); - boolean success = oldFile.renameTo(newFile); if (!success) { - Log.w(TAG, "renaming "+ path + " to " + newPath + " failed"); return MtpConstants.RESPONSE_GENERAL_ERROR; } - // finally update database + // finally update MediaProvider ContentValues values = new ContentValues(); - values.put(Files.FileColumns.DATA, newPath); - int updated = 0; + values.put(Files.FileColumns.DATA, newPath.toString()); + String[] whereArgs = new String[]{oldPath.toString()}; try { // note - we are relying on a special case in MediaProvider.update() to update // the paths for all children in the case where this is a directory. - updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs); + mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs); } catch (RemoteException e) { Log.e(TAG, "RemoteException in mMediaProvider.update", e); } - if (updated == 0) { - Log.e(TAG, "Unable to update path for " + path + " to " + newPath); - // this shouldn't happen, but if it does we need to rename the file to its original name - newFile.renameTo(oldFile); - return MtpConstants.RESPONSE_GENERAL_ERROR; - } // check if nomedia status changed - if (newFile.isDirectory()) { + if (obj.isDir()) { // for directories, check if renamed from something hidden to something non-hidden - if (oldFile.getName().startsWith(".") && !newPath.startsWith(".")) { + if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) { // directory was unhidden try { - mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath, null); + mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath.toString(), null); } catch (RemoteException e) { Log.e(TAG, "failed to unhide/rescan for " + newPath); } } } else { // for files, check if renamed from .nomedia to something else - if (oldFile.getName().toLowerCase(Locale.US).equals(".nomedia") - && !newPath.toLowerCase(Locale.US).equals(".nomedia")) { + if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA) + && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) { try { - mMediaProvider.call(MediaStore.UNHIDE_CALL, oldFile.getParent(), null); + mMediaProvider.call(MediaStore.UNHIDE_CALL, + oldPath.getParent().toString(), null); } catch (RemoteException e) { Log.e(TAG, "failed to unhide/rescan for " + newPath); } } } - return MtpConstants.RESPONSE_OK; } - private int moveObject(int handle, int newParent, int newStorage, String newPath) { - String[] whereArgs = new String[] { Integer.toString(handle) }; + private int beginMoveObject(int handle, int newParent, int newStorage) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + MtpStorageManager.MtpObject parent = newParent == 0 ? + mManager.getStorageRoot(newStorage) : mManager.getObject(newParent); + if (obj == null || parent == null) + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; + + boolean allowed = mManager.beginMoveObject(obj, parent); + return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR; + } - // do not allow renaming any of the special subdirectories - if (isStorageSubDirectory(newPath)) { - return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; + private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage, + int objId, boolean success) { + MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ? + mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent); + MtpStorageManager.MtpObject newParentObj = newParent == 0 ? + mManager.getStorageRoot(newStorage) : mManager.getObject(newParent); + MtpStorageManager.MtpObject obj = mManager.getObject(objId); + String name = obj.getName(); + if (newParentObj == null || oldParentObj == null + ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) { + Log.e(TAG, "Failed to end move object"); + return; } - // update database + obj = mManager.getObject(objId); + if (!success || obj == null) + return; + // Get parent info from MediaProvider, since the id is different from MTP's ContentValues values = new ContentValues(); - values.put(Files.FileColumns.DATA, newPath); - values.put(Files.FileColumns.PARENT, newParent); - values.put(Files.FileColumns.STORAGE_ID, newStorage); - int updated = 0; + Path path = newParentObj.getPath().resolve(name); + Path oldPath = oldParentObj.getPath().resolve(name); + values.put(Files.FileColumns.DATA, path.toString()); + if (obj.getParent().isRoot()) { + values.put(Files.FileColumns.PARENT, 0); + } else { + int parentId = findInMedia(path.getParent()); + if (parentId != -1) { + values.put(Files.FileColumns.PARENT, parentId); + } else { + // The new parent isn't in MediaProvider, so delete the object instead + deleteFromMedia(oldPath, obj.isDir()); + return; + } + } + // update MediaProvider + Cursor c = null; + String[] whereArgs = new String[]{oldPath.toString()}; try { - // note - we are relying on a special case in MediaProvider.update() to update - // the paths for all children in the case where this is a directory. - updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs); + int parentId = -1; + if (!oldParentObj.isRoot()) { + parentId = findInMedia(oldPath.getParent()); + } + if (oldParentObj.isRoot() || parentId != -1) { + // Old parent exists in MediaProvider - perform a move + // note - we are relying on a special case in MediaProvider.update() to update + // the paths for all children in the case where this is a directory. + mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs); + } else { + // Old parent doesn't exist - add the object + values.put(Files.FileColumns.FORMAT, obj.getFormat()); + values.put(Files.FileColumns.SIZE, obj.getSize()); + values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime()); + Uri uri = mMediaProvider.insert(mObjectsUri, values); + if (uri != null) { + rescanFile(path.toString(), + Integer.parseInt(uri.getPathSegments().get(2)), obj.getFormat()); + } + } } catch (RemoteException e) { Log.e(TAG, "RemoteException in mMediaProvider.update", e); } - if (updated == 0) { - Log.e(TAG, "Unable to update path for " + handle + " to " + newPath); - return MtpConstants.RESPONSE_GENERAL_ERROR; + } + + private int beginCopyObject(int handle, int newParent, int newStorage) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + MtpStorageManager.MtpObject parent = newParent == 0 ? + mManager.getStorageRoot(newStorage) : mManager.getObject(newParent); + if (obj == null || parent == null) + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; + return mManager.beginCopyObject(obj, parent); + } + + private void endCopyObject(int handle, boolean success) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null || !mManager.endCopyObject(obj, success)) { + Log.e(TAG, "Failed to end copy object"); + return; + } + if (!success) { + return; + } + String path = obj.getPath().toString(); + int format = obj.getFormat(); + // Get parent info from MediaProvider, since the id is different from MTP's + ContentValues values = new ContentValues(); + values.put(Files.FileColumns.DATA, path); + values.put(Files.FileColumns.FORMAT, format); + values.put(Files.FileColumns.SIZE, obj.getSize()); + values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime()); + try { + if (obj.getParent().isRoot()) { + values.put(Files.FileColumns.PARENT, 0); + } else { + int parentId = findInMedia(obj.getParent().getPath()); + if (parentId != -1) { + values.put(Files.FileColumns.PARENT, parentId); + } else { + // The parent isn't in MediaProvider. Don't add the new file. + return; + } + } + if (obj.isDir()) { + mMediaScanner.scanDirectories(new String[]{path}); + } else { + Uri uri = mMediaProvider.insert(mObjectsUri, values); + if (uri != null) { + rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format); + } + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in beginSendObject", e); } - return MtpConstants.RESPONSE_OK; } private int setObjectProperty(int handle, int property, - long intValue, String stringValue) { + long intValue, String stringValue) { switch (property) { case MtpConstants.PROPERTY_OBJECT_FILE_NAME: return renameFile(handle, stringValue); @@ -912,24 +748,23 @@ public class MtpDatabase implements AutoCloseable { value.getChars(0, length, outStringValue, 0); outStringValue[length] = 0; return MtpConstants.RESPONSE_OK; - case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE: // use screen size as max image size - Display display = ((WindowManager)mContext.getSystemService( + Display display = ((WindowManager) mContext.getSystemService( Context.WINDOW_SERVICE)).getDefaultDisplay(); int width = display.getMaximumSizeDimension(); int height = display.getMaximumSizeDimension(); - String imageSize = Integer.toString(width) + "x" + Integer.toString(height); + String imageSize = Integer.toString(width) + "x" + Integer.toString(height); imageSize.getChars(0, imageSize.length(), outStringValue, 0); outStringValue[imageSize.length()] = 0; return MtpConstants.RESPONSE_OK; - case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE: outIntValue[0] = mDeviceType; return MtpConstants.RESPONSE_OK; - - // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code - + case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL: + outIntValue[0] = mBatteryLevel; + outIntValue[1] = mBatteryScale; + return MtpConstants.RESPONSE_OK; default: return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED; } @@ -950,179 +785,144 @@ public class MtpDatabase implements AutoCloseable { } private boolean getObjectInfo(int handle, int[] outStorageFormatParent, - char[] outName, long[] outCreatedModified) { - Cursor c = null; - try { - c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null, null); - if (c != null && c.moveToNext()) { - outStorageFormatParent[0] = c.getInt(1); - outStorageFormatParent[1] = c.getInt(2); - outStorageFormatParent[2] = c.getInt(3); - - // extract name from path - String path = c.getString(4); - int lastSlash = path.lastIndexOf('/'); - int start = (lastSlash >= 0 ? lastSlash + 1 : 0); - int end = path.length(); - if (end - start > 255) { - end = start + 255; - } - path.getChars(start, end, outName, 0); - outName[end - start] = 0; - - outCreatedModified[0] = c.getLong(5); - outCreatedModified[1] = c.getLong(6); - // use modification date as creation date if date added is not set - if (outCreatedModified[0] == 0) { - outCreatedModified[0] = outCreatedModified[1]; - } - return true; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectInfo", e); - } finally { - if (c != null) { - c.close(); - } + char[] outName, long[] outCreatedModified) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return false; } - return false; + outStorageFormatParent[0] = obj.getStorageId(); + outStorageFormatParent[1] = obj.getFormat(); + outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId(); + + int nameLen = Integer.min(obj.getName().length(), 255); + obj.getName().getChars(0, nameLen, outName, 0); + outName[nameLen] = 0; + + outCreatedModified[0] = obj.getModifiedTime(); + outCreatedModified[1] = obj.getModifiedTime(); + return true; } private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) { - if (handle == 0) { - // special case root directory - mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0); - outFilePath[mMediaStoragePath.length()] = 0; - outFileLengthFormat[0] = 0; - outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION; - return MtpConstants.RESPONSE_OK; + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; } - Cursor c = null; - try { - c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null, null); - if (c != null && c.moveToNext()) { - String path = c.getString(1); - path.getChars(0, path.length(), outFilePath, 0); - outFilePath[path.length()] = 0; - // File transfers from device to host will likely fail if the size is incorrect. - // So to be safe, use the actual file size here. - outFileLengthFormat[0] = new File(path).length(); - outFileLengthFormat[1] = c.getLong(2); - return MtpConstants.RESPONSE_OK; - } else { - return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectFilePath", e); + + String path = obj.getPath().toString(); + int pathLen = Integer.min(path.length(), 4096); + path.getChars(0, pathLen, outFilePath, 0); + outFilePath[pathLen] = 0; + + outFileLengthFormat[0] = obj.getSize(); + outFileLengthFormat[1] = obj.getFormat(); + return MtpConstants.RESPONSE_OK; + } + + private int getObjectFormat(int handle) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return -1; + } + return obj.getFormat(); + } + + private int beginDeleteObject(int handle) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; + } + if (!mManager.beginRemoveObject(obj)) { return MtpConstants.RESPONSE_GENERAL_ERROR; - } finally { - if (c != null) { - c.close(); - } } + return MtpConstants.RESPONSE_OK; } - private int getObjectFormat(int handle) { + private void endDeleteObject(int handle, boolean success) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return; + } + if (!mManager.endRemoveObject(obj, success)) + Log.e(TAG, "Failed to end remove object"); + if (success) + deleteFromMedia(obj.getPath(), obj.isDir()); + } + + private int findInMedia(Path path) { + int ret = -1; Cursor c = null; try { - c = mMediaProvider.query(mObjectsUri, FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null, null); + c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE, + new String[]{path.toString()}, null, null); if (c != null && c.moveToNext()) { - return c.getInt(1); - } else { - return -1; + ret = c.getInt(0); } } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectFilePath", e); - return -1; + Log.e(TAG, "Error finding " + path + " in MediaProvider"); } finally { - if (c != null) { + if (c != null) c.close(); - } } + return ret; } - private int deleteFile(int handle) { - mDatabaseModified = true; - String path = null; - int format = 0; - - Cursor c = null; + private void deleteFromMedia(Path path, boolean isDir) { try { - c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null, null); - if (c != null && c.moveToNext()) { - // don't convert to media path here, since we will be matching - // against paths in the database matching /data/media - path = c.getString(1); - format = c.getInt(2); - } else { - return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; - } - - if (path == null || format == 0) { - return MtpConstants.RESPONSE_GENERAL_ERROR; - } - - // do not allow deleting any of the special subdirectories - if (isStorageSubDirectory(path)) { - return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; - } - - if (format == MtpConstants.FORMAT_ASSOCIATION) { + // Delete the object(s) from MediaProvider, but ignore errors. + if (isDir) { // recursive case - delete all children first - Uri uri = Files.getMtpObjectsUri(mVolumeName); - int count = mMediaProvider.delete(uri, - // the 'like' makes it use the index, the 'lower()' makes it correct - // when the path contains sqlite wildcard characters - "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", - new String[] { path + "/%",Integer.toString(path.length() + 1), path + "/"}); + mMediaProvider.delete(mObjectsUri, + // the 'like' makes it use the index, the 'lower()' makes it correct + // when the path contains sqlite wildcard characters + "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", + new String[]{path + "/%", Integer.toString(path.toString().length() + 1), + path.toString() + "/"}); } - Uri uri = Files.getMtpObjectsUri(mVolumeName, handle); - if (mMediaProvider.delete(uri, null, null) > 0) { - if (format != MtpConstants.FORMAT_ASSOCIATION - && path.toLowerCase(Locale.US).endsWith("/.nomedia")) { + String[] whereArgs = new String[]{path.toString()}; + if (mMediaProvider.delete(mObjectsUri, PATH_WHERE, whereArgs) > 0) { + if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) { try { - String parentPath = path.substring(0, path.lastIndexOf("/")); + String parentPath = path.getParent().toString(); mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null); } catch (RemoteException e) { Log.e(TAG, "failed to unhide/rescan for " + path); } } - return MtpConstants.RESPONSE_OK; } else { - return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in deleteFile", e); - return MtpConstants.RESPONSE_GENERAL_ERROR; - } finally { - if (c != null) { - c.close(); + Log.i(TAG, "Mediaprovider didn't delete " + path); } + } catch (Exception e) { + Log.d(TAG, "Failed to delete " + path + " from MediaProvider"); } } private int[] getObjectReferences(int handle) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) + return null; + // Translate this handle to the MediaProvider Handle + handle = findInMedia(obj.getPath()); + if (handle == -1) + return null; Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); Cursor c = null; try { - c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null, null); + c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null); if (c == null) { return null; } - int count = c.getCount(); - if (count > 0) { - int[] result = new int[count]; - for (int i = 0; i < count; i++) { - c.moveToNext(); - result[i] = c.getInt(0); + ArrayList<Integer> result = new ArrayList<>(); + while (c.moveToNext()) { + // Translate result handles back into handles for this session. + String refPath = c.getString(0); + MtpStorageManager.MtpObject refObj = mManager.getByPath(refPath); + if (refObj != null) { + result.add(refObj.getId()); + } } - return result; - } + return result.stream().mapToInt(Integer::intValue).toArray(); } catch (RemoteException e) { Log.e(TAG, "RemoteException in getObjectList", e); } finally { @@ -1134,17 +934,29 @@ public class MtpDatabase implements AutoCloseable { } private int setObjectReferences(int handle, int[] references) { - mDatabaseModified = true; + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; + // Translate this handle to the MediaProvider Handle + handle = findInMedia(obj.getPath()); + if (handle == -1) + return MtpConstants.RESPONSE_GENERAL_ERROR; Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); - int count = references.length; - ContentValues[] valuesList = new ContentValues[count]; - for (int i = 0; i < count; i++) { + ArrayList<ContentValues> valuesList = new ArrayList<>(); + for (int id : references) { + // Translate each reference id to the MediaProvider Id + MtpStorageManager.MtpObject refObj = mManager.getObject(id); + if (refObj == null) + continue; + int refHandle = findInMedia(refObj.getPath()); + if (refHandle == -1) + continue; ContentValues values = new ContentValues(); - values.put(Files.FileColumns._ID, references[i]); - valuesList[i] = values; + values.put(Files.FileColumns._ID, refHandle); + valuesList.add(values); } try { - if (mMediaProvider.bulkInsert(uri, valuesList) > 0) { + if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) { return MtpConstants.RESPONSE_OK; } } catch (RemoteException e) { @@ -1153,17 +965,6 @@ public class MtpDatabase implements AutoCloseable { return MtpConstants.RESPONSE_GENERAL_ERROR; } - private void sessionStarted() { - mDatabaseModified = false; - } - - private void sessionEnded() { - if (mDatabaseModified) { - mUserContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END)); - mDatabaseModified = false; - } - } - // used by the JNI code private long mNativeContext; diff --git a/media/java/android/mtp/MtpPropertyGroup.java b/media/java/android/mtp/MtpPropertyGroup.java index dea300838385..77d0f34f1ad6 100644 --- a/media/java/android/mtp/MtpPropertyGroup.java +++ b/media/java/android/mtp/MtpPropertyGroup.java @@ -23,22 +23,21 @@ import android.os.RemoteException; import android.provider.MediaStore.Audio; import android.provider.MediaStore.Files; import android.provider.MediaStore.Images; -import android.provider.MediaStore.MediaColumns; import android.util.Log; import java.util.ArrayList; +/** + * MtpPropertyGroup represents a list of MTP properties. + * {@hide} + */ class MtpPropertyGroup { - - private static final String TAG = "MtpPropertyGroup"; + private static final String TAG = MtpPropertyGroup.class.getSimpleName(); private class Property { - // MTP property code - int code; - // MTP data type - int type; - // column index for our query - int column; + int code; + int type; + int column; Property(int code, int type, int column) { this.code = code; @@ -47,32 +46,26 @@ class MtpPropertyGroup { } } - private final MtpDatabase mDatabase; private final ContentProviderClient mProvider; private final String mVolumeName; private final Uri mUri; // list of all properties in this group - private final Property[] mProperties; + private final Property[] mProperties; // list of columns for database query - private String[] mColumns; + private String[] mColumns; + + private static final String PATH_WHERE = Files.FileColumns.DATA + "=?"; - private static final String ID_WHERE = Files.FileColumns._ID + "=?"; - private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?"; - private static final String ID_FORMAT_WHERE = ID_WHERE + " AND " + FORMAT_WHERE; - private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?"; - private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " + FORMAT_WHERE; // constructs a property group for a list of properties - public MtpPropertyGroup(MtpDatabase database, ContentProviderClient provider, String volumeName, - int[] properties) { - mDatabase = database; + public MtpPropertyGroup(ContentProviderClient provider, String volumeName, int[] properties) { mProvider = provider; mVolumeName = volumeName; mUri = Files.getMtpObjectsUri(volumeName); int count = properties.length; - ArrayList<String> columns = new ArrayList<String>(count); + ArrayList<String> columns = new ArrayList<>(count); columns.add(Files.FileColumns._ID); mProperties = new Property[count]; @@ -90,37 +83,29 @@ class MtpPropertyGroup { String column = null; int type; - switch (code) { + switch (code) { case MtpConstants.PROPERTY_STORAGE_ID: - column = Files.FileColumns.STORAGE_ID; type = MtpConstants.TYPE_UINT32; break; - case MtpConstants.PROPERTY_OBJECT_FORMAT: - column = Files.FileColumns.FORMAT; + case MtpConstants.PROPERTY_OBJECT_FORMAT: type = MtpConstants.TYPE_UINT16; break; case MtpConstants.PROPERTY_PROTECTION_STATUS: - // protection status is always 0 type = MtpConstants.TYPE_UINT16; break; case MtpConstants.PROPERTY_OBJECT_SIZE: - column = Files.FileColumns.SIZE; type = MtpConstants.TYPE_UINT64; break; case MtpConstants.PROPERTY_OBJECT_FILE_NAME: - column = Files.FileColumns.DATA; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_NAME: - column = MediaColumns.TITLE; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_DATE_MODIFIED: - column = Files.FileColumns.DATE_MODIFIED; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_DATE_ADDED: - column = Files.FileColumns.DATE_ADDED; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE: @@ -128,12 +113,9 @@ class MtpPropertyGroup { type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_PARENT_OBJECT: - column = Files.FileColumns.PARENT; type = MtpConstants.TYPE_UINT32; break; case MtpConstants.PROPERTY_PERSISTENT_UID: - // PUID is concatenation of storageID and object handle - column = Files.FileColumns.STORAGE_ID; type = MtpConstants.TYPE_UINT128; break; case MtpConstants.PROPERTY_DURATION: @@ -145,7 +127,6 @@ class MtpPropertyGroup { type = MtpConstants.TYPE_UINT16; break; case MtpConstants.PROPERTY_DISPLAY_NAME: - column = MediaColumns.DISPLAY_NAME; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_ARTIST: @@ -195,40 +176,19 @@ class MtpPropertyGroup { } } - private String queryString(int id, String column) { - Cursor c = null; - try { - // for now we are only reading properties from the "objects" table - c = mProvider.query(mUri, - new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null, null); - if (c != null && c.moveToNext()) { - return c.getString(1); - } else { - return ""; - } - } catch (Exception e) { - return null; - } finally { - if (c != null) { - c.close(); - } - } - } - - private String queryAudio(int id, String column) { + private String queryAudio(String path, String column) { Cursor c = null; try { c = mProvider.query(Audio.Media.getContentUri(mVolumeName), - new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null, null); + new String [] { column }, + PATH_WHERE, new String[] {path}, null, null); if (c != null && c.moveToNext()) { - return c.getString(1); + return c.getString(0); } else { return ""; } } catch (Exception e) { - return null; + return ""; } finally { if (c != null) { c.close(); @@ -236,21 +196,19 @@ class MtpPropertyGroup { } } - private String queryGenre(int id) { + private String queryGenre(String path) { Cursor c = null; try { - Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id); - c = mProvider.query(uri, - new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME }, - null, null, null, null); + c = mProvider.query(Audio.Genres.getContentUri(mVolumeName), + new String [] { Audio.GenresColumns.NAME }, + PATH_WHERE, new String[] {path}, null, null); if (c != null && c.moveToNext()) { - return c.getString(1); + return c.getString(0); } else { return ""; } } catch (Exception e) { - Log.e(TAG, "queryGenre exception", e); - return null; + return ""; } finally { if (c != null) { c.close(); @@ -258,211 +216,127 @@ class MtpPropertyGroup { } } - private Long queryLong(int id, String column) { - Cursor c = null; - try { - // for now we are only reading properties from the "objects" table - c = mProvider.query(mUri, - new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null, null); - if (c != null && c.moveToNext()) { - return new Long(c.getLong(1)); - } - } catch (Exception e) { - } finally { - if (c != null) { - c.close(); - } - } - return null; - } - - private static String nameFromPath(String path) { - // extract name from full path - int start = 0; - int lastSlash = path.lastIndexOf('/'); - if (lastSlash >= 0) { - start = lastSlash + 1; - } - int end = path.length(); - if (end - start > 255) { - end = start + 255; - } - return path.substring(start, end); - } - - MtpPropertyList getPropertyList(int handle, int format, int depth) { - //Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth); - if (depth > 1) { - // we only support depth 0 and 1 - // depth 0: single object, depth 1: immediate children - return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED); - } - - String where; - String[] whereArgs; - if (format == 0) { - if (handle == 0xFFFFFFFF) { - // select all objects - where = null; - whereArgs = null; - } else { - whereArgs = new String[] { Integer.toString(handle) }; - if (depth == 1) { - where = PARENT_WHERE; - } else { - where = ID_WHERE; - } - } - } else { - if (handle == 0xFFFFFFFF) { - // select all objects with given format - where = FORMAT_WHERE; - whereArgs = new String[] { Integer.toString(format) }; - } else { - whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) }; - if (depth == 1) { - where = PARENT_FORMAT_WHERE; - } else { - where = ID_FORMAT_WHERE; - } - } - } - + /** + * Gets the values of the properties represented by this property group for the given + * object and adds them to the given property list. + * @return Response_OK if the operation succeeded. + */ + public int getPropertyList(MtpStorageManager.MtpObject object, MtpPropertyList list) { Cursor c = null; - try { - // don't query if not necessary - if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) { - c = mProvider.query(mUri, mColumns, where, whereArgs, null, null); - if (c == null) { - return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); + int id = object.getId(); + String path = object.getPath().toString(); + for (Property property : mProperties) { + if (property.column != -1 && c == null) { + try { + // Look up the entry in MediaProvider only if one of those properties is needed. + c = mProvider.query(mUri, mColumns, + PATH_WHERE, new String[] {path}, null, null); + if (c != null && !c.moveToNext()) { + c.close(); + c = null; + } + } catch (RemoteException e) { + Log.e(TAG, "Mediaprovider lookup failed"); } } - - int count = (c == null ? 1 : c.getCount()); - MtpPropertyList result = new MtpPropertyList(count * mProperties.length, - MtpConstants.RESPONSE_OK); - - // iterate over all objects in the query - for (int objectIndex = 0; objectIndex < count; objectIndex++) { - if (c != null) { - c.moveToNext(); - handle = (int)c.getLong(0); - } - - // iterate over all properties in the query for the given object - for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) { - Property property = mProperties[propertyIndex]; - int propertyCode = property.code; - int column = property.column; - - // handle some special cases - switch (propertyCode) { - case MtpConstants.PROPERTY_PROTECTION_STATUS: - // protection status is always 0 - result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0); - break; - case MtpConstants.PROPERTY_OBJECT_FILE_NAME: - // special case - need to extract file name from full path - String value = c.getString(column); - if (value != null) { - result.append(handle, propertyCode, nameFromPath(value)); - } else { - result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); - } - break; - case MtpConstants.PROPERTY_NAME: - // first try title - String name = c.getString(column); - // then try name - if (name == null) { - name = queryString(handle, Audio.PlaylistsColumns.NAME); - } - // if title and name fail, extract name from full path - if (name == null) { - name = queryString(handle, Files.FileColumns.DATA); - if (name != null) { - name = nameFromPath(name); - } - } - if (name != null) { - result.append(handle, propertyCode, name); - } else { - result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); - } - break; - case MtpConstants.PROPERTY_DATE_MODIFIED: - case MtpConstants.PROPERTY_DATE_ADDED: - // convert from seconds to DateTime - result.append(handle, propertyCode, format_date_time(c.getInt(column))); - break; - case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE: - // release date is stored internally as just the year - int year = c.getInt(column); - String dateTime = Integer.toString(year) + "0101T000000"; - result.append(handle, propertyCode, dateTime); - break; - case MtpConstants.PROPERTY_PERSISTENT_UID: - // PUID is concatenation of storageID and object handle - long puid = c.getLong(column); - puid <<= 32; - puid += handle; - result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid); - break; - case MtpConstants.PROPERTY_TRACK: - result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, - c.getInt(column) % 1000); - break; - case MtpConstants.PROPERTY_ARTIST: - result.append(handle, propertyCode, - queryAudio(handle, Audio.AudioColumns.ARTIST)); - break; - case MtpConstants.PROPERTY_ALBUM_NAME: - result.append(handle, propertyCode, - queryAudio(handle, Audio.AudioColumns.ALBUM)); - break; - case MtpConstants.PROPERTY_GENRE: - String genre = queryGenre(handle); - if (genre != null) { - result.append(handle, propertyCode, genre); - } else { - result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); - } - break; - case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC: - case MtpConstants.PROPERTY_AUDIO_BITRATE: - case MtpConstants.PROPERTY_SAMPLE_RATE: - // we don't have these in our database, so return 0 - result.append(handle, propertyCode, MtpConstants.TYPE_UINT32, 0); + switch (property.code) { + case MtpConstants.PROPERTY_PROTECTION_STATUS: + // protection status is always 0 + list.append(id, property.code, property.type, 0); + break; + case MtpConstants.PROPERTY_NAME: + case MtpConstants.PROPERTY_OBJECT_FILE_NAME: + case MtpConstants.PROPERTY_DISPLAY_NAME: + list.append(id, property.code, object.getName()); + break; + case MtpConstants.PROPERTY_DATE_MODIFIED: + case MtpConstants.PROPERTY_DATE_ADDED: + // convert from seconds to DateTime + list.append(id, property.code, + format_date_time(object.getModifiedTime())); + break; + case MtpConstants.PROPERTY_STORAGE_ID: + list.append(id, property.code, property.type, object.getStorageId()); + break; + case MtpConstants.PROPERTY_OBJECT_FORMAT: + list.append(id, property.code, property.type, object.getFormat()); + break; + case MtpConstants.PROPERTY_OBJECT_SIZE: + list.append(id, property.code, property.type, object.getSize()); + break; + case MtpConstants.PROPERTY_PARENT_OBJECT: + list.append(id, property.code, property.type, + object.getParent().isRoot() ? 0 : object.getParent().getId()); + break; + case MtpConstants.PROPERTY_PERSISTENT_UID: + // The persistent uid must be unique and never reused among all objects, + // and remain the same between sessions. + long puid = (object.getPath().toString().hashCode() << 32) + + object.getModifiedTime(); + list.append(id, property.code, property.type, puid); + break; + case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE: + // release date is stored internally as just the year + int year = 0; + if (c != null) + year = c.getInt(property.column); + String dateTime = Integer.toString(year) + "0101T000000"; + list.append(id, property.code, dateTime); + break; + case MtpConstants.PROPERTY_TRACK: + int track = 0; + if (c != null) + track = c.getInt(property.column); + list.append(id, property.code, MtpConstants.TYPE_UINT16, + track % 1000); + break; + case MtpConstants.PROPERTY_ARTIST: + list.append(id, property.code, + queryAudio(path, Audio.AudioColumns.ARTIST)); + break; + case MtpConstants.PROPERTY_ALBUM_NAME: + list.append(id, property.code, + queryAudio(path, Audio.AudioColumns.ALBUM)); + break; + case MtpConstants.PROPERTY_GENRE: + String genre = queryGenre(path); + if (genre != null) { + list.append(id, property.code, genre); + } + break; + case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC: + case MtpConstants.PROPERTY_AUDIO_BITRATE: + case MtpConstants.PROPERTY_SAMPLE_RATE: + // we don't have these in our database, so return 0 + list.append(id, property.code, MtpConstants.TYPE_UINT32, 0); + break; + case MtpConstants.PROPERTY_BITRATE_TYPE: + case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS: + // we don't have these in our database, so return 0 + list.append(id, property.code, MtpConstants.TYPE_UINT16, 0); + break; + default: + switch(property.type) { + case MtpConstants.TYPE_UNDEFINED: + list.append(id, property.code, property.type, 0); break; - case MtpConstants.PROPERTY_BITRATE_TYPE: - case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS: - // we don't have these in our database, so return 0 - result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0); + case MtpConstants.TYPE_STR: + String value = ""; + if (c != null) + value = c.getString(property.column); + list.append(id, property.code, value); break; default: - if (property.type == MtpConstants.TYPE_STR) { - result.append(handle, propertyCode, c.getString(column)); - } else if (property.type == MtpConstants.TYPE_UNDEFINED) { - result.append(handle, propertyCode, property.type, 0); - } else { - result.append(handle, propertyCode, property.type, - c.getLong(column)); - } - break; + long longValue = 0L; + if (c != null) + longValue = c.getLong(property.column); + list.append(id, property.code, property.type, longValue); } - } - } - - return result; - } catch (RemoteException e) { - return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR); - } finally { - if (c != null) { - c.close(); } } - // impossible to get here, so no return statement + if (c != null) + c.close(); + return MtpConstants.RESPONSE_OK; } private native String format_date_time(long seconds); diff --git a/media/java/android/mtp/MtpPropertyList.java b/media/java/android/mtp/MtpPropertyList.java index f9bc603e3de0..ede90dac517c 100644 --- a/media/java/android/mtp/MtpPropertyList.java +++ b/media/java/android/mtp/MtpPropertyList.java @@ -16,6 +16,9 @@ package android.mtp; +import java.util.ArrayList; +import java.util.List; + /** * Encapsulates the ObjectPropList dataset used by the GetObjectPropList command. * The fields of this class are read by JNI code in android_media_MtpDatabase.cpp @@ -23,56 +26,70 @@ package android.mtp; class MtpPropertyList { - // number of results returned - private int mCount; - // maximum number of results - private final int mMaxCount; - // result code for GetObjectPropList - public int mResult; // list of object handles (first field in quadruplet) - public final int[] mObjectHandles; - // list of object propery codes (second field in quadruplet) - public final int[] mPropertyCodes; + private List<Integer> mObjectHandles; + // list of object property codes (second field in quadruplet) + private List<Integer> mPropertyCodes; // list of data type codes (third field in quadruplet) - public final int[] mDataTypes; + private List<Integer> mDataTypes; // list of long int property values (fourth field in quadruplet, when value is integer type) - public long[] mLongValues; + private List<Long> mLongValues; // list of long int property values (fourth field in quadruplet, when value is string type) - public String[] mStringValues; - - // constructor only called from MtpDatabase - public MtpPropertyList(int maxCount, int result) { - mMaxCount = maxCount; - mResult = result; - mObjectHandles = new int[maxCount]; - mPropertyCodes = new int[maxCount]; - mDataTypes = new int[maxCount]; - // mLongValues and mStringValues are created lazily since both might not be necessary + private List<String> mStringValues; + + // Return value of this operation + private int mCode; + + public MtpPropertyList(int code) { + mCode = code; + mObjectHandles = new ArrayList<>(); + mPropertyCodes = new ArrayList<>(); + mDataTypes = new ArrayList<>(); + mLongValues = new ArrayList<>(); + mStringValues = new ArrayList<>(); } public void append(int handle, int property, int type, long value) { - int index = mCount++; - if (mLongValues == null) { - mLongValues = new long[mMaxCount]; - } - mObjectHandles[index] = handle; - mPropertyCodes[index] = property; - mDataTypes[index] = type; - mLongValues[index] = value; + mObjectHandles.add(handle); + mPropertyCodes.add(property); + mDataTypes.add(type); + mLongValues.add(value); + mStringValues.add(null); } public void append(int handle, int property, String value) { - int index = mCount++; - if (mStringValues == null) { - mStringValues = new String[mMaxCount]; - } - mObjectHandles[index] = handle; - mPropertyCodes[index] = property; - mDataTypes[index] = MtpConstants.TYPE_STR; - mStringValues[index] = value; + mObjectHandles.add(handle); + mPropertyCodes.add(property); + mDataTypes.add(MtpConstants.TYPE_STR); + mStringValues.add(value); + mLongValues.add(0L); + } + + public int getCode() { + return mCode; + } + + public int getCount() { + return mObjectHandles.size(); + } + + public int[] getObjectHandles() { + return mObjectHandles.stream().mapToInt(Integer::intValue).toArray(); + } + + public int[] getPropertyCodes() { + return mPropertyCodes.stream().mapToInt(Integer::intValue).toArray(); + } + + public int[] getDataTypes() { + return mDataTypes.stream().mapToInt(Integer::intValue).toArray(); + } + + public long[] getLongValues() { + return mLongValues.stream().mapToLong(Long::longValue).toArray(); } - public void setResult(int result) { - mResult = result; + public String[] getStringValues() { + return mStringValues.toArray(new String[0]); } } diff --git a/media/java/android/mtp/MtpStorage.java b/media/java/android/mtp/MtpStorage.java index 6ca442c7e66f..c72b827d8a2d 100644 --- a/media/java/android/mtp/MtpStorage.java +++ b/media/java/android/mtp/MtpStorage.java @@ -31,15 +31,13 @@ public class MtpStorage { private final int mStorageId; private final String mPath; private final String mDescription; - private final long mReserveSpace; private final boolean mRemovable; private final long mMaxFileSize; - public MtpStorage(StorageVolume volume, Context context) { - mStorageId = volume.getStorageId(); + public MtpStorage(StorageVolume volume, int storageId) { + mStorageId = storageId; mPath = volume.getPath(); - mDescription = volume.getDescription(context); - mReserveSpace = volume.getMtpReserveSpace() * 1024L * 1024L; + mDescription = volume.getDescription(null); mRemovable = volume.isRemovable(); mMaxFileSize = volume.getMaxFileSize(); } @@ -72,16 +70,6 @@ public class MtpStorage { } /** - * Returns the amount of space to reserve on the storage file system. - * This can be set to a non-zero value to prevent MTP from filling up the entire storage. - * - * @return reserved space in bytes. - */ - public final long getReserveSpace() { - return mReserveSpace; - } - - /** * Returns true if the storage is removable. * * @return is removable diff --git a/media/java/android/mtp/MtpStorageManager.java b/media/java/android/mtp/MtpStorageManager.java new file mode 100644 index 000000000000..bdc87413288a --- /dev/null +++ b/media/java/android/mtp/MtpStorageManager.java @@ -0,0 +1,1210 @@ +/* + * Copyright (C) 2017 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.mtp; + +import android.media.MediaFile; +import android.os.FileObserver; +import android.os.storage.StorageVolume; +import android.util.Log; + +import java.io.IOException; +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +/** + * MtpStorageManager provides functionality for listing, tracking, and notifying MtpServer of + * filesystem changes. As directories are listed, this class will cache the results, + * and send events when objects are added/removed from cached directories. + * {@hide} + */ +public class MtpStorageManager { + private static final String TAG = MtpStorageManager.class.getSimpleName(); + public static boolean sDebug = false; + + // Inotify flags not provided by FileObserver + private static final int IN_ONLYDIR = 0x01000000; + private static final int IN_Q_OVERFLOW = 0x00004000; + private static final int IN_IGNORED = 0x00008000; + private static final int IN_ISDIR = 0x40000000; + + private class MtpObjectObserver extends FileObserver { + MtpObject mObject; + + MtpObjectObserver(MtpObject object) { + super(object.getPath().toString(), + MOVED_FROM | MOVED_TO | DELETE | CREATE | IN_ONLYDIR); + mObject = object; + } + + @Override + public void onEvent(int event, String path) { + synchronized (MtpStorageManager.this) { + if ((event & IN_Q_OVERFLOW) != 0) { + // We are out of space in the inotify queue. + Log.e(TAG, "Received Inotify overflow event!"); + } + MtpObject obj = mObject.getChild(path); + if ((event & MOVED_TO) != 0 || (event & CREATE) != 0) { + if (sDebug) + Log.i(TAG, "Got inotify added event for " + path + " " + event); + handleAddedObject(mObject, path, (event & IN_ISDIR) != 0); + } else if ((event & MOVED_FROM) != 0 || (event & DELETE) != 0) { + if (obj == null) { + Log.w(TAG, "Object was null in event " + path); + return; + } + if (sDebug) + Log.i(TAG, "Got inotify removed event for " + path + " " + event); + handleRemovedObject(obj); + } else if ((event & IN_IGNORED) != 0) { + if (sDebug) + Log.i(TAG, "inotify for " + mObject.getPath() + " deleted"); + if (mObject.mObserver != null) + mObject.mObserver.stopWatching(); + mObject.mObserver = null; + } else { + Log.w(TAG, "Got unrecognized event " + path + " " + event); + } + } + } + + @Override + public void finalize() { + // If the server shuts down and starts up again, the new server's observers can be + // invalidated by the finalize() calls of the previous server's observers. + // Hence, disable the automatic stopWatching() call in FileObserver#finalize, and + // always call stopWatching() manually whenever an observer should be shut down. + } + } + + /** + * Describes how the object is being acted on, to determine how events are handled. + */ + private enum MtpObjectState { + NORMAL, + FROZEN, // Object is going to be modified in this session. + FROZEN_ADDED, // Object was frozen, and has been added. + FROZEN_REMOVED, // Object was frozen, and has been removed. + FROZEN_ONESHOT_ADD, // Object is waiting for single add event before being unfrozen. + FROZEN_ONESHOT_DEL, // Object is waiting for single remove event and will then be removed. + } + + /** + * Describes the current operation being done on an object. Determines whether observers are + * created on new folders. + */ + private enum MtpOperation { + NONE, // Any new folders not added as part of the session are immediately observed. + ADD, // New folders added as part of the session are immediately observed. + RENAME, // Renamed or moved folders are not immediately observed. + COPY, // Copied folders are immediately observed iff the original was. + DELETE, // Exists for debugging purposes only. + } + + /** MtpObject represents either a file or directory in an associated storage. **/ + public static class MtpObject { + // null for root objects + private MtpObject mParent; + + private String mName; + private int mId; + private MtpObjectState mState; + private MtpOperation mOp; + + private boolean mVisited; + private boolean mIsDir; + + // null if not a directory + private HashMap<String, MtpObject> mChildren; + // null if not both a directory and visited + private FileObserver mObserver; + + MtpObject(String name, int id, MtpObject parent, boolean isDir) { + mId = id; + mName = name; + mParent = parent; + mObserver = null; + mVisited = false; + mState = MtpObjectState.NORMAL; + mIsDir = isDir; + mOp = MtpOperation.NONE; + + mChildren = mIsDir ? new HashMap<>() : null; + } + + /** Public methods for getting object info **/ + + public String getName() { + return mName; + } + + public int getId() { + return mId; + } + + public boolean isDir() { + return mIsDir; + } + + public int getFormat() { + return mIsDir ? MtpConstants.FORMAT_ASSOCIATION : MediaFile.getFormatCode(mName, null); + } + + public int getStorageId() { + return getRoot().getId(); + } + + public long getModifiedTime() { + return getPath().toFile().lastModified() / 1000; + } + + public MtpObject getParent() { + return mParent; + } + + public MtpObject getRoot() { + return isRoot() ? this : mParent.getRoot(); + } + + public long getSize() { + return mIsDir ? 0 : getPath().toFile().length(); + } + + public Path getPath() { + return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName); + } + + public boolean isRoot() { + return mParent == null; + } + + /** For MtpStorageManager only **/ + + private void setName(String name) { + mName = name; + } + + private void setId(int id) { + mId = id; + } + + private boolean isVisited() { + return mVisited; + } + + private void setParent(MtpObject parent) { + mParent = parent; + } + + private void setDir(boolean dir) { + if (dir != mIsDir) { + mIsDir = dir; + mChildren = mIsDir ? new HashMap<>() : null; + } + } + + private void setVisited(boolean visited) { + mVisited = visited; + } + + private MtpObjectState getState() { + return mState; + } + + private void setState(MtpObjectState state) { + mState = state; + if (mState == MtpObjectState.NORMAL) + mOp = MtpOperation.NONE; + } + + private MtpOperation getOperation() { + return mOp; + } + + private void setOperation(MtpOperation op) { + mOp = op; + } + + private FileObserver getObserver() { + return mObserver; + } + + private void setObserver(FileObserver observer) { + mObserver = observer; + } + + private void addChild(MtpObject child) { + mChildren.put(child.getName(), child); + } + + private MtpObject getChild(String name) { + return mChildren.get(name); + } + + private Collection<MtpObject> getChildren() { + return mChildren.values(); + } + + private boolean exists() { + return getPath().toFile().exists(); + } + + private MtpObject copy(boolean recursive) { + MtpObject copy = new MtpObject(mName, mId, mParent, mIsDir); + copy.mIsDir = mIsDir; + copy.mVisited = mVisited; + copy.mState = mState; + copy.mChildren = mIsDir ? new HashMap<>() : null; + if (recursive && mIsDir) { + for (MtpObject child : mChildren.values()) { + MtpObject childCopy = child.copy(true); + childCopy.setParent(copy); + copy.addChild(childCopy); + } + } + return copy; + } + } + + /** + * A class that processes generated filesystem events. + */ + public static abstract class MtpNotifier { + /** + * Called when an object is added. + */ + public abstract void sendObjectAdded(int id); + + /** + * Called when an object is deleted. + */ + public abstract void sendObjectRemoved(int id); + } + + private MtpNotifier mMtpNotifier; + + // A cache of MtpObjects. The objects in the cache are keyed by object id. + // The root object of each storage isn't in this map since they all have ObjectId 0. + // Instead, they can be found in mRoots keyed by storageId. + private HashMap<Integer, MtpObject> mObjects; + + // A cache of the root MtpObject for each storage, keyed by storage id. + private HashMap<Integer, MtpObject> mRoots; + + // Object and Storage ids are allocated incrementally and not to be reused. + private int mNextObjectId; + private int mNextStorageId; + + // Special subdirectories. When set, only return objects rooted in these directories, and do + // not allow them to be modified. + private Set<String> mSubdirectories; + + private volatile boolean mCheckConsistency; + private Thread mConsistencyThread; + + public MtpStorageManager(MtpNotifier notifier, Set<String> subdirectories) { + mMtpNotifier = notifier; + mSubdirectories = subdirectories; + mObjects = new HashMap<>(); + mRoots = new HashMap<>(); + mNextObjectId = 1; + mNextStorageId = 1; + + mCheckConsistency = false; // Set to true to turn on automatic consistency checking + mConsistencyThread = new Thread(() -> { + while (mCheckConsistency) { + try { + Thread.sleep(15 * 1000); + } catch (InterruptedException e) { + return; + } + if (MtpStorageManager.this.checkConsistency()) { + Log.v(TAG, "Cache is consistent"); + } else { + Log.w(TAG, "Cache is not consistent"); + } + } + }); + if (mCheckConsistency) + mConsistencyThread.start(); + } + + /** + * Clean up resources used by the storage manager. + */ + public synchronized void close() { + Stream<MtpObject> objs = Stream.concat(mRoots.values().stream(), + mObjects.values().stream()); + + Iterator<MtpObject> iter = objs.iterator(); + while (iter.hasNext()) { + // Close all FileObservers. + MtpObject obj = iter.next(); + if (obj.getObserver() != null) { + obj.getObserver().stopWatching(); + obj.setObserver(null); + } + } + + // Shut down the consistency checking thread + if (mCheckConsistency) { + mCheckConsistency = false; + mConsistencyThread.interrupt(); + try { + mConsistencyThread.join(); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Sets the special subdirectories, which are the subdirectories of root storage that queries + * are restricted to. Must be done before any root storages are accessed. + * @param subDirs Subdirectories to set, or null to reset. + */ + public synchronized void setSubdirectories(Set<String> subDirs) { + mSubdirectories = subDirs; + } + + /** + * Allocates an MTP storage id for the given volume and add it to current roots. + * @param volume Storage to add. + * @return the associated MtpStorage + */ + public synchronized MtpStorage addMtpStorage(StorageVolume volume) { + int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1; + MtpObject root = new MtpObject(volume.getPath(), storageId, null, true); + MtpStorage storage = new MtpStorage(volume, storageId); + mRoots.put(storageId, root); + return storage; + } + + /** + * Removes the given storage and all associated items from the cache. + * @param storage Storage to remove. + */ + public synchronized void removeMtpStorage(MtpStorage storage) { + removeObjectFromCache(getStorageRoot(storage.getStorageId()), true, true); + } + + /** + * Checks if the given object can be renamed, moved, or deleted. + * If there are special subdirectories, they cannot be modified. + * @param obj Object to check. + * @return Whether object can be modified. + */ + private synchronized boolean isSpecialSubDir(MtpObject obj) { + return obj.getParent().isRoot() && mSubdirectories != null + && !mSubdirectories.contains(obj.getName()); + } + + /** + * Get the object with the specified path. Visit any necessary directories on the way. + * @param path Full path of the object to find. + * @return The desired object, or null if it cannot be found. + */ + public synchronized MtpObject getByPath(String path) { + MtpObject obj = null; + for (MtpObject root : mRoots.values()) { + if (path.startsWith(root.getName())) { + obj = root; + path = path.substring(root.getName().length()); + } + } + for (String name : path.split("/")) { + if (obj == null || !obj.isDir()) + return null; + if ("".equals(name)) + continue; + if (!obj.isVisited()) + getChildren(obj); + obj = obj.getChild(name); + } + return obj; + } + + /** + * Get the object with specified id. + * @param id Id of object. must not be 0 or 0xFFFFFFFF + * @return Object, or null if error. + */ + public synchronized MtpObject getObject(int id) { + if (id == 0 || id == 0xFFFFFFFF) { + Log.w(TAG, "Can't get root storages with getObject()"); + return null; + } + if (!mObjects.containsKey(id)) { + Log.w(TAG, "Id " + id + " doesn't exist"); + return null; + } + return mObjects.get(id); + } + + /** + * Get the storage with specified id. + * @param id Storage id. + * @return Object that is the root of the storage, or null if error. + */ + public MtpObject getStorageRoot(int id) { + if (!mRoots.containsKey(id)) { + Log.w(TAG, "StorageId " + id + " doesn't exist"); + return null; + } + return mRoots.get(id); + } + + private int getNextObjectId() { + int ret = mNextObjectId; + // Treat the id as unsigned int + mNextObjectId = (int) ((long) mNextObjectId + 1); + return ret; + } + + private int getNextStorageId() { + return mNextStorageId++; + } + + /** + * Get all objects matching the given parent, format, and storage + * @param parent object id of the parent. 0 for all objects, 0xFFFFFFFF for all object in root + * @param format format of returned objects. 0 for any format + * @param storageId storage id to look in. 0xFFFFFFFF for all storages + * @return A stream of matched objects, or null if error + */ + public synchronized Stream<MtpObject> getObjects(int parent, int format, int storageId) { + boolean recursive = parent == 0; + if (parent == 0xFFFFFFFF) + parent = 0; + if (storageId == 0xFFFFFFFF) { + // query all stores + if (parent == 0) { + // Get the objects of this format and parent in each store. + ArrayList<Stream<MtpObject>> streamList = new ArrayList<>(); + for (MtpObject root : mRoots.values()) { + streamList.add(getObjects(root, format, recursive)); + } + return Stream.of(streamList).flatMap(Collection::stream).reduce(Stream::concat) + .orElseGet(Stream::empty); + } + } + MtpObject obj = parent == 0 ? getStorageRoot(storageId) : getObject(parent); + if (obj == null) + return null; + return getObjects(obj, format, recursive); + } + + private synchronized Stream<MtpObject> getObjects(MtpObject parent, int format, boolean rec) { + Collection<MtpObject> children = getChildren(parent); + if (children == null) + return null; + Stream<MtpObject> ret = Stream.of(children).flatMap(Collection::stream); + + if (format != 0) { + ret = ret.filter(o -> o.getFormat() == format); + } + if (rec) { + // Get all objects recursively. + ArrayList<Stream<MtpObject>> streamList = new ArrayList<>(); + streamList.add(ret); + for (MtpObject o : children) { + if (o.isDir()) + streamList.add(getObjects(o, format, true)); + } + ret = Stream.of(streamList).filter(Objects::nonNull).flatMap(Collection::stream) + .reduce(Stream::concat).orElseGet(Stream::empty); + } + return ret; + } + + /** + * Return the children of the given object. If the object hasn't been visited yet, add + * its children to the cache and start observing it. + * @param object the parent object + * @return The collection of child objects or null if error + */ + private synchronized Collection<MtpObject> getChildren(MtpObject object) { + if (object == null || !object.isDir()) { + Log.w(TAG, "Can't find children of " + (object == null ? "null" : object.getId())); + return null; + } + if (!object.isVisited()) { + Path dir = object.getPath(); + /* + * If a file is added after the observer starts watching the directory, but before + * the contents are listed, it will generate an event that will get processed + * after this synchronized function returns. We handle this by ignoring object + * added events if an object at that path already exists. + */ + if (object.getObserver() != null) + Log.e(TAG, "Observer is not null!"); + object.setObserver(new MtpObjectObserver(object)); + object.getObserver().startWatching(); + try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { + for (Path file : stream) { + addObjectToCache(object, file.getFileName().toString(), + file.toFile().isDirectory()); + } + } catch (IOException | DirectoryIteratorException e) { + Log.e(TAG, e.toString()); + object.getObserver().stopWatching(); + object.setObserver(null); + return null; + } + object.setVisited(true); + } + return object.getChildren(); + } + + /** + * Create a new object from the given path and add it to the cache. + * @param parent The parent object + * @param newName Path of the new object + * @return the new object if success, else null + */ + private synchronized MtpObject addObjectToCache(MtpObject parent, String newName, + boolean isDir) { + if (!parent.isRoot() && getObject(parent.getId()) != parent) + // parent object has been removed + return null; + if (parent.getChild(newName) != null) { + // Object already exists + return null; + } + if (mSubdirectories != null && parent.isRoot() && !mSubdirectories.contains(newName)) { + // Not one of the restricted subdirectories. + return null; + } + + MtpObject obj = new MtpObject(newName, getNextObjectId(), parent, isDir); + mObjects.put(obj.getId(), obj); + parent.addChild(obj); + return obj; + } + + /** + * Remove the given path from the cache. + * @param removed The removed object + * @param removeGlobal Whether to remove the object from the global id map + * @param recursive Whether to also remove its children recursively. + * @return true if successfully removed + */ + private synchronized boolean removeObjectFromCache(MtpObject removed, boolean removeGlobal, + boolean recursive) { + boolean ret = removed.isRoot() + || removed.getParent().mChildren.remove(removed.getName(), removed); + if (!ret && sDebug) + Log.w(TAG, "Failed to remove from parent " + removed.getPath()); + if (removed.isRoot()) { + ret = mRoots.remove(removed.getId(), removed) && ret; + } else if (removeGlobal) { + ret = mObjects.remove(removed.getId(), removed) && ret; + } + if (!ret && sDebug) + Log.w(TAG, "Failed to remove from global cache " + removed.getPath()); + if (removed.getObserver() != null) { + removed.getObserver().stopWatching(); + removed.setObserver(null); + } + if (removed.isDir() && recursive) { + // Remove all descendants from cache recursively + Collection<MtpObject> children = new ArrayList<>(removed.getChildren()); + for (MtpObject child : children) { + ret = removeObjectFromCache(child, removeGlobal, true) && ret; + } + } + return ret; + } + + private synchronized void handleAddedObject(MtpObject parent, String path, boolean isDir) { + MtpOperation op = MtpOperation.NONE; + MtpObject obj = parent.getChild(path); + if (obj != null) { + MtpObjectState state = obj.getState(); + op = obj.getOperation(); + if (obj.isDir() != isDir && state != MtpObjectState.FROZEN_REMOVED) + Log.d(TAG, "Inconsistent directory info! " + obj.getPath()); + obj.setDir(isDir); + switch (state) { + case FROZEN: + case FROZEN_REMOVED: + obj.setState(MtpObjectState.FROZEN_ADDED); + break; + case FROZEN_ONESHOT_ADD: + obj.setState(MtpObjectState.NORMAL); + break; + case NORMAL: + case FROZEN_ADDED: + // This can happen when handling listed object in a new directory. + return; + default: + Log.w(TAG, "Unexpected state in add " + path + " " + state); + } + if (sDebug) + Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op); + } else { + obj = MtpStorageManager.this.addObjectToCache(parent, path, isDir); + if (obj != null) { + MtpStorageManager.this.mMtpNotifier.sendObjectAdded(obj.getId()); + } else { + if (sDebug) + Log.w(TAG, "object " + path + " already exists"); + return; + } + } + if (isDir) { + // If this was added as part of a rename do not visit or send events. + if (op == MtpOperation.RENAME) + return; + + // If it was part of a copy operation, then only add observer if it was visited before. + if (op == MtpOperation.COPY && !obj.isVisited()) + return; + + if (obj.getObserver() != null) { + Log.e(TAG, "Observer is not null!"); + return; + } + obj.setObserver(new MtpObjectObserver(obj)); + obj.getObserver().startWatching(); + obj.setVisited(true); + + // It's possible that objects were added to a watched directory before the watch can be + // created, so manually handle those. + try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) { + for (Path file : stream) { + if (sDebug) + Log.i(TAG, "Manually handling event for " + file.getFileName().toString()); + handleAddedObject(obj, file.getFileName().toString(), + file.toFile().isDirectory()); + } + } catch (IOException | DirectoryIteratorException e) { + Log.e(TAG, e.toString()); + obj.getObserver().stopWatching(); + obj.setObserver(null); + } + } + } + + private synchronized void handleRemovedObject(MtpObject obj) { + MtpObjectState state = obj.getState(); + MtpOperation op = obj.getOperation(); + switch (state) { + case FROZEN_ADDED: + obj.setState(MtpObjectState.FROZEN_REMOVED); + break; + case FROZEN_ONESHOT_DEL: + removeObjectFromCache(obj, op != MtpOperation.RENAME, false); + break; + case FROZEN: + obj.setState(MtpObjectState.FROZEN_REMOVED); + break; + case NORMAL: + if (MtpStorageManager.this.removeObjectFromCache(obj, true, true)) + MtpStorageManager.this.mMtpNotifier.sendObjectRemoved(obj.getId()); + break; + default: + // This shouldn't happen; states correspond to objects that don't exist + Log.e(TAG, "Got unexpected object remove for " + obj.getName()); + } + if (sDebug) + Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op); + } + + /** + * Block the caller until all events currently in the event queue have been + * read and processed. Used for testing purposes. + */ + public void flushEvents() { + try { + // TODO make this smarter + Thread.sleep(500); + } catch (InterruptedException e) { + + } + } + + /** + * Dumps a representation of the cache to log. + */ + public synchronized void dump() { + for (int key : mObjects.keySet()) { + MtpObject obj = mObjects.get(key); + Log.i(TAG, key + " | " + (obj.getParent() == null ? obj.getParent().getId() : "null") + + " | " + obj.getName() + " | " + (obj.isDir() ? "dir" : "obj") + + " | " + (obj.isVisited() ? "v" : "nv") + " | " + obj.getState()); + } + } + + /** + * Checks consistency of the cache. This checks whether all objects have correct links + * to their parent, and whether directories are missing or have extraneous objects. + * @return true iff cache is consistent + */ + public synchronized boolean checkConsistency() { + Stream<MtpObject> objs = Stream.concat(mRoots.values().stream(), + mObjects.values().stream()); + Iterator<MtpObject> iter = objs.iterator(); + boolean ret = true; + while (iter.hasNext()) { + MtpObject obj = iter.next(); + if (!obj.exists()) { + Log.w(TAG, "Object doesn't exist " + obj.getPath() + " " + obj.getId()); + ret = false; + } + if (obj.getState() != MtpObjectState.NORMAL) { + Log.w(TAG, "Object " + obj.getPath() + " in state " + obj.getState()); + ret = false; + } + if (obj.getOperation() != MtpOperation.NONE) { + Log.w(TAG, "Object " + obj.getPath() + " in operation " + obj.getOperation()); + ret = false; + } + if (!obj.isRoot() && mObjects.get(obj.getId()) != obj) { + Log.w(TAG, "Object " + obj.getPath() + " is not in map correctly"); + ret = false; + } + if (obj.getParent() != null) { + if (obj.getParent().isRoot() && obj.getParent() + != mRoots.get(obj.getParent().getId())) { + Log.w(TAG, "Root parent is not in root mapping " + obj.getPath()); + ret = false; + } + if (!obj.getParent().isRoot() && obj.getParent() + != mObjects.get(obj.getParent().getId())) { + Log.w(TAG, "Parent is not in object mapping " + obj.getPath()); + ret = false; + } + if (obj.getParent().getChild(obj.getName()) != obj) { + Log.w(TAG, "Child does not exist in parent " + obj.getPath()); + ret = false; + } + } + if (obj.isDir()) { + if (obj.isVisited() == (obj.getObserver() == null)) { + Log.w(TAG, obj.getPath() + " is " + (obj.isVisited() ? "" : "not ") + + " visited but observer is " + obj.getObserver()); + ret = false; + } + if (!obj.isVisited() && obj.getChildren().size() > 0) { + Log.w(TAG, obj.getPath() + " is not visited but has children"); + ret = false; + } + try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) { + Set<String> files = new HashSet<>(); + for (Path file : stream) { + if (obj.isVisited() && + obj.getChild(file.getFileName().toString()) == null && + (mSubdirectories == null || !obj.isRoot() || + mSubdirectories.contains(file.getFileName().toString()))) { + Log.w(TAG, "File exists in fs but not in children " + file); + ret = false; + } + files.add(file.toString()); + } + for (MtpObject child : obj.getChildren()) { + if (!files.contains(child.getPath().toString())) { + Log.w(TAG, "File in children doesn't exist in fs " + child.getPath()); + ret = false; + } + if (child != mObjects.get(child.getId())) { + Log.w(TAG, "Child is not in object map " + child.getPath()); + ret = false; + } + } + } catch (IOException | DirectoryIteratorException e) { + Log.w(TAG, e.toString()); + ret = false; + } + } + } + return ret; + } + + /** + * Informs MtpStorageManager that an object with the given path is about to be added. + * @param parent The parent object of the object to be added. + * @param name Filename of object to add. + * @return Object id of the added object, or -1 if it cannot be added. + */ + public synchronized int beginSendObject(MtpObject parent, String name, int format) { + if (sDebug) + Log.v(TAG, "beginSendObject " + name); + if (!parent.isDir()) + return -1; + if (parent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name)) + return -1; + getChildren(parent); // Ensure parent is visited + MtpObject obj = addObjectToCache(parent, name, format == MtpConstants.FORMAT_ASSOCIATION); + if (obj == null) + return -1; + obj.setState(MtpObjectState.FROZEN); + obj.setOperation(MtpOperation.ADD); + return obj.getId(); + } + + /** + * Clean up the object state after a sendObject operation. + * @param obj The object, returned from beginAddObject(). + * @param succeeded Whether the file was successfully created. + * @return Whether cache state was successfully cleaned up. + */ + public synchronized boolean endSendObject(MtpObject obj, boolean succeeded) { + if (sDebug) + Log.v(TAG, "endSendObject " + succeeded); + return generalEndAddObject(obj, succeeded, true); + } + + /** + * Informs MtpStorageManager that the given object is about to be renamed. + * If this returns true, it must be followed with an endRenameObject() + * @param obj Object to be renamed. + * @param newName New name of the object. + * @return Whether renaming is allowed. + */ + public synchronized boolean beginRenameObject(MtpObject obj, String newName) { + if (sDebug) + Log.v(TAG, "beginRenameObject " + obj.getName() + " " + newName); + if (obj.isRoot()) + return false; + if (isSpecialSubDir(obj)) + return false; + if (obj.getParent().getChild(newName) != null) + // Object already exists in parent with that name. + return false; + + MtpObject oldObj = obj.copy(false); + obj.setName(newName); + obj.getParent().addChild(obj); + oldObj.getParent().addChild(oldObj); + return generalBeginRenameObject(oldObj, obj); + } + + /** + * Cleans up cache state after a rename operation and sends any events that were missed. + * @param obj The object being renamed, the same one that was passed in beginRenameObject(). + * @param oldName The previous name of the object. + * @param success Whether the rename operation succeeded. + * @return Whether state was successfully cleaned up. + */ + public synchronized boolean endRenameObject(MtpObject obj, String oldName, boolean success) { + if (sDebug) + Log.v(TAG, "endRenameObject " + success); + MtpObject parent = obj.getParent(); + MtpObject oldObj = parent.getChild(oldName); + if (!success) { + // If the rename failed, we want oldObj to be the original and obj to be the dummy. + // Switch the objects, except for their name and state. + MtpObject temp = oldObj; + MtpObjectState oldState = oldObj.getState(); + temp.setName(obj.getName()); + temp.setState(obj.getState()); + oldObj = obj; + oldObj.setName(oldName); + oldObj.setState(oldState); + obj = temp; + parent.addChild(obj); + parent.addChild(oldObj); + } + return generalEndRenameObject(oldObj, obj, success); + } + + /** + * Informs MtpStorageManager that the given object is about to be deleted by the initiator, + * so don't send an event. + * @param obj Object to be deleted. + * @return Whether cache deletion is allowed. + */ + public synchronized boolean beginRemoveObject(MtpObject obj) { + if (sDebug) + Log.v(TAG, "beginRemoveObject " + obj.getName()); + return !obj.isRoot() && !isSpecialSubDir(obj) + && generalBeginRemoveObject(obj, MtpOperation.DELETE); + } + + /** + * Clean up cache state after a delete operation and send any events that were missed. + * @param obj Object to be deleted, same one passed in beginRemoveObject(). + * @param success Whether operation was completed successfully. + * @return Whether cache state is correct. + */ + public synchronized boolean endRemoveObject(MtpObject obj, boolean success) { + if (sDebug) + Log.v(TAG, "endRemoveObject " + success); + boolean ret = true; + if (obj.isDir()) { + for (MtpObject child : new ArrayList<>(obj.getChildren())) + if (child.getOperation() == MtpOperation.DELETE) + ret = endRemoveObject(child, success) && ret; + } + return generalEndRemoveObject(obj, success, true) && ret; + } + + /** + * Informs MtpStorageManager that the given object is about to be moved to a new parent. + * @param obj Object to be moved. + * @param newParent The new parent object. + * @return Whether the move is allowed. + */ + public synchronized boolean beginMoveObject(MtpObject obj, MtpObject newParent) { + if (sDebug) + Log.v(TAG, "beginMoveObject " + newParent.getPath()); + if (obj.isRoot()) + return false; + if (isSpecialSubDir(obj)) + return false; + getChildren(newParent); // Ensure parent is visited + if (newParent.getChild(obj.getName()) != null) + // Object already exists in parent with that name. + return false; + if (obj.getStorageId() != newParent.getStorageId()) { + /* + * The move is occurring across storages. The observers will not remain functional + * after the move, and the move will not be atomic. We have to copy the file tree + * to the destination and recreate the observers once copy is complete. + */ + MtpObject newObj = obj.copy(true); + newObj.setParent(newParent); + newParent.addChild(newObj); + return generalBeginRemoveObject(obj, MtpOperation.RENAME) + && generalBeginCopyObject(newObj, false); + } + // Move obj to new parent, create a dummy object in the old parent. + MtpObject oldObj = obj.copy(false); + obj.setParent(newParent); + oldObj.getParent().addChild(oldObj); + obj.getParent().addChild(obj); + return generalBeginRenameObject(oldObj, obj); + } + + /** + * Clean up cache state after a move operation and send any events that were missed. + * @param oldParent The old parent object. + * @param newParent The new parent object. + * @param name The name of the object being moved. + * @param success Whether operation was completed successfully. + * @return Whether cache state is correct. + */ + public synchronized boolean endMoveObject(MtpObject oldParent, MtpObject newParent, String name, + boolean success) { + if (sDebug) + Log.v(TAG, "endMoveObject " + success); + MtpObject oldObj = oldParent.getChild(name); + MtpObject newObj = newParent.getChild(name); + if (oldObj == null || newObj == null) + return false; + if (oldParent.getStorageId() != newObj.getStorageId()) { + boolean ret = endRemoveObject(oldObj, success); + return generalEndCopyObject(newObj, success, true) && ret; + } + if (!success) { + // If the rename failed, we want oldObj to be the original and obj to be the dummy. + // Switch the objects, except for their parent and state. + MtpObject temp = oldObj; + MtpObjectState oldState = oldObj.getState(); + temp.setParent(newObj.getParent()); + temp.setState(newObj.getState()); + oldObj = newObj; + oldObj.setParent(oldParent); + oldObj.setState(oldState); + newObj = temp; + newObj.getParent().addChild(newObj); + oldParent.addChild(oldObj); + } + return generalEndRenameObject(oldObj, newObj, success); + } + + /** + * Informs MtpStorageManager that the given object is about to be copied recursively. + * @param object Object to be copied + * @param newParent New parent for the object. + * @return The object id for the new copy, or -1 if error. + */ + public synchronized int beginCopyObject(MtpObject object, MtpObject newParent) { + if (sDebug) + Log.v(TAG, "beginCopyObject " + object.getName() + " to " + newParent.getPath()); + String name = object.getName(); + if (!newParent.isDir()) + return -1; + if (newParent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name)) + return -1; + getChildren(newParent); // Ensure parent is visited + if (newParent.getChild(name) != null) + return -1; + MtpObject newObj = object.copy(object.isDir()); + newParent.addChild(newObj); + newObj.setParent(newParent); + if (!generalBeginCopyObject(newObj, true)) + return -1; + return newObj.getId(); + } + + /** + * Cleans up cache state after a copy operation. + * @param object Object that was copied. + * @param success Whether the operation was successful. + * @return Whether cache state is consistent. + */ + public synchronized boolean endCopyObject(MtpObject object, boolean success) { + if (sDebug) + Log.v(TAG, "endCopyObject " + object.getName() + " " + success); + return generalEndCopyObject(object, success, false); + } + + private synchronized boolean generalEndAddObject(MtpObject obj, boolean succeeded, + boolean removeGlobal) { + switch (obj.getState()) { + case FROZEN: + // Object was never created. + if (succeeded) { + // The operation was successful so the event must still be in the queue. + obj.setState(MtpObjectState.FROZEN_ONESHOT_ADD); + } else { + // The operation failed and never created the file. + if (!removeObjectFromCache(obj, removeGlobal, false)) { + return false; + } + } + break; + case FROZEN_ADDED: + obj.setState(MtpObjectState.NORMAL); + if (!succeeded) { + MtpObject parent = obj.getParent(); + // The operation failed but some other process created the file. Send an event. + if (!removeObjectFromCache(obj, removeGlobal, false)) + return false; + handleAddedObject(parent, obj.getName(), obj.isDir()); + } + // else: The operation successfully created the object. + break; + case FROZEN_REMOVED: + if (!removeObjectFromCache(obj, removeGlobal, false)) + return false; + if (succeeded) { + // Some other process deleted the object. Send an event. + mMtpNotifier.sendObjectRemoved(obj.getId()); + } + // else: Mtp deleted the object as part of cleanup. Don't send an event. + break; + default: + return false; + } + return true; + } + + private synchronized boolean generalEndRemoveObject(MtpObject obj, boolean success, + boolean removeGlobal) { + switch (obj.getState()) { + case FROZEN: + if (success) { + // Object was deleted successfully, and event is still in the queue. + obj.setState(MtpObjectState.FROZEN_ONESHOT_DEL); + } else { + // Object was not deleted. + obj.setState(MtpObjectState.NORMAL); + } + break; + case FROZEN_ADDED: + // Object was deleted, and then readded. + obj.setState(MtpObjectState.NORMAL); + if (success) { + // Some other process readded the object. + MtpObject parent = obj.getParent(); + if (!removeObjectFromCache(obj, removeGlobal, false)) + return false; + handleAddedObject(parent, obj.getName(), obj.isDir()); + } + // else : Object still exists after failure. + break; + case FROZEN_REMOVED: + if (!removeObjectFromCache(obj, removeGlobal, false)) + return false; + if (!success) { + // Some other process deleted the object. + mMtpNotifier.sendObjectRemoved(obj.getId()); + } + // else : This process deleted the object as part of the operation. + break; + default: + return false; + } + return true; + } + + private synchronized boolean generalBeginRenameObject(MtpObject fromObj, MtpObject toObj) { + fromObj.setState(MtpObjectState.FROZEN); + toObj.setState(MtpObjectState.FROZEN); + fromObj.setOperation(MtpOperation.RENAME); + toObj.setOperation(MtpOperation.RENAME); + return true; + } + + private synchronized boolean generalEndRenameObject(MtpObject fromObj, MtpObject toObj, + boolean success) { + boolean ret = generalEndRemoveObject(fromObj, success, !success); + return generalEndAddObject(toObj, success, success) && ret; + } + + private synchronized boolean generalBeginRemoveObject(MtpObject obj, MtpOperation op) { + obj.setState(MtpObjectState.FROZEN); + obj.setOperation(op); + if (obj.isDir()) { + for (MtpObject child : obj.getChildren()) + generalBeginRemoveObject(child, op); + } + return true; + } + + private synchronized boolean generalBeginCopyObject(MtpObject obj, boolean newId) { + obj.setState(MtpObjectState.FROZEN); + obj.setOperation(MtpOperation.COPY); + if (newId) { + obj.setId(getNextObjectId()); + mObjects.put(obj.getId(), obj); + } + if (obj.isDir()) + for (MtpObject child : obj.getChildren()) + if (!generalBeginCopyObject(child, newId)) + return false; + return true; + } + + private synchronized boolean generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal) { + if (success && addGlobal) + mObjects.put(obj.getId(), obj); + boolean ret = true; + if (obj.isDir()) { + for (MtpObject child : new ArrayList<>(obj.getChildren())) { + if (child.getOperation() == MtpOperation.COPY) + ret = generalEndCopyObject(child, success, addGlobal) && ret; + } + } + ret = generalEndAddObject(obj, success, success || !addGlobal) && ret; + return ret; + } +} |