summaryrefslogtreecommitdiff
path: root/media/java
diff options
context:
space:
mode:
Diffstat (limited to 'media/java')
-rw-r--r--media/java/android/media/AudioAttributes.java55
-rw-r--r--media/java/android/media/AudioDeviceInfo.java78
-rw-r--r--media/java/android/media/AudioFocusInfo.java23
-rw-r--r--media/java/android/media/AudioFocusRequest.java37
-rw-r--r--media/java/android/media/AudioFormat.java85
-rw-r--r--media/java/android/media/AudioManager.java512
-rw-r--r--media/java/android/media/AudioPort.java3
-rw-r--r--media/java/android/media/AudioPortEventHandler.java26
-rw-r--r--media/java/android/media/AudioPresentation.java181
-rw-r--r--media/java/android/media/AudioRecord.java150
-rw-r--r--media/java/android/media/AudioSystem.java16
-rw-r--r--media/java/android/media/AudioTrack.java293
-rw-r--r--media/java/android/media/BufferingParams.java384
-rw-r--r--media/java/android/media/DataSourceDesc.java465
-rw-r--r--media/java/android/media/ExifInterface.java86
-rw-r--r--media/java/android/media/IAudioFocusDispatcher.aidl2
-rw-r--r--media/java/android/media/IAudioServerStateDispatcher.aidl28
-rw-r--r--media/java/android/media/IAudioService.aidl43
-rw-r--r--media/java/android/media/ISessionTokensListener.aidl (renamed from media/java/android/media/IPlayer.aidl)16
-rw-r--r--media/java/android/media/ImageReader.java1
-rw-r--r--media/java/android/media/Media2DataSource.java62
-rw-r--r--media/java/android/media/Media2HTTPConnection.java385
-rw-r--r--media/java/android/media/Media2HTTPService.java98
-rw-r--r--media/java/android/media/MediaActionSound.java27
-rw-r--r--media/java/android/media/MediaBrowser2.java217
-rw-r--r--media/java/android/media/MediaCodecInfo.java5
-rw-r--r--media/java/android/media/MediaController2.java644
-rw-r--r--media/java/android/media/MediaDataSource.java4
-rw-r--r--media/java/android/media/MediaDrm.java806
-rw-r--r--media/java/android/media/MediaExtractor.java19
-rw-r--r--media/java/android/media/MediaFormat.java115
-rw-r--r--media/java/android/media/MediaItem2.java159
-rw-r--r--media/java/android/media/MediaLibraryService2.java379
-rw-r--r--media/java/android/media/MediaMetadata.java47
-rw-r--r--media/java/android/media/MediaMetadata2.java683
-rw-r--r--media/java/android/media/MediaMetadataRetriever.java164
-rw-r--r--media/java/android/media/MediaMuxer.java16
-rw-r--r--media/java/android/media/MediaPlayer.java276
-rw-r--r--media/java/android/media/MediaPlayer2.java2438
-rw-r--r--media/java/android/media/MediaPlayer2Impl.java4928
-rw-r--r--media/java/android/media/MediaPlayerInterface.java91
-rw-r--r--media/java/android/media/MediaRecorder.java171
-rw-r--r--media/java/android/media/MediaRouter.java6
-rw-r--r--media/java/android/media/MediaScanner.java6
-rw-r--r--media/java/android/media/MediaSession2.java1377
-rw-r--r--media/java/android/media/MediaSessionService2.java235
-rw-r--r--media/java/android/media/MicrophoneInfo.java342
-rw-r--r--media/java/android/media/NativeRoutingEventHandlerDelegate.java51
-rw-r--r--media/java/android/media/PlaybackState2.java230
-rw-r--r--media/java/android/media/Rating2.java273
-rw-r--r--media/java/android/media/RingtoneManager.java15
-rw-r--r--media/java/android/media/SessionPlayer2.java152
-rw-r--r--media/java/android/media/SessionToken2.java160
-rw-r--r--media/java/android/media/VolumePolicy.java2
-rw-r--r--media/java/android/media/VolumeProvider2.java151
-rw-r--r--media/java/android/media/audiofx/Visualizer.java25
-rw-r--r--media/java/android/media/audiopolicy/AudioPolicy.java67
-rw-r--r--media/java/android/media/audiopolicy/IAudioPolicyCallback.aidl3
-rw-r--r--media/java/android/media/browse/MediaBrowser.java15
-rw-r--r--media/java/android/media/midi/package.html13
-rw-r--r--media/java/android/media/projection/MediaProjectionManager.java9
-rw-r--r--media/java/android/media/session/ISessionManager.aidl10
-rw-r--r--media/java/android/media/session/MediaSession.java22
-rw-r--r--media/java/android/media/session/MediaSessionManager.java247
-rw-r--r--media/java/android/media/session/PlaybackState.java3
-rw-r--r--media/java/android/media/tv/ITvInputHardware.aidl6
-rw-r--r--media/java/android/media/tv/TvContract.java28
-rw-r--r--media/java/android/media/tv/TvInputManager.java11
-rw-r--r--media/java/android/media/update/ApiLoader.java60
-rw-r--r--media/java/android/media/update/MediaBrowser2Provider.java34
-rw-r--r--media/java/android/media/update/MediaControlView2Provider.java44
-rw-r--r--media/java/android/media/update/MediaController2Provider.java77
-rw-r--r--media/java/android/media/update/MediaItem2Provider.java37
-rw-r--r--media/java/android/media/update/MediaLibraryService2Provider.java41
-rw-r--r--media/java/android/media/update/MediaMetadata2Provider.java37
-rw-r--r--media/java/android/media/update/MediaSession2Provider.java126
-rw-r--r--media/java/android/media/update/MediaSessionService2Provider.java42
-rw-r--r--media/java/android/media/update/PlaybackState2Provider.java43
-rw-r--r--media/java/android/media/update/ProviderCreator.java (renamed from media/java/android/media/VolumeShaper.aidl)12
-rw-r--r--media/java/android/media/update/Rating2Provider.java37
-rw-r--r--media/java/android/media/update/SessionPlayer2Provider.java55
-rw-r--r--media/java/android/media/update/SessionToken2Provider.java34
-rw-r--r--media/java/android/media/update/StaticProvider.java142
-rw-r--r--media/java/android/media/update/TransportControlProvider.java38
-rw-r--r--media/java/android/media/update/VideoView2Provider.java81
-rw-r--r--media/java/android/media/update/ViewGroupHelper.java369
-rw-r--r--media/java/android/media/update/ViewGroupProvider.java67
-rw-r--r--media/java/android/media/update/VolumeProvider2Provider.java27
-rwxr-xr-xmedia/java/android/mtp/MtpDatabase.java1339
-rw-r--r--media/java/android/mtp/MtpPropertyGroup.java404
-rw-r--r--media/java/android/mtp/MtpPropertyList.java95
-rw-r--r--media/java/android/mtp/MtpStorage.java18
-rw-r--r--media/java/android/mtp/MtpStorageManager.java1210
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>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaLibraryService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/service&gt;</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 &lt;uses-permission&gt;}
+ * 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 &lt;uses-permission&gt;}
+ * 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>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaSessionService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/service&gt;</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>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaSessionService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;meta-data android:name="android.media.session"
+ * android:value="session_id"/&gt;
+ * &lt;/service&gt;</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 &lt; 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 &ldquo;input&rdquo; and &ldquo;output&rdquo; are from the standpoint of the device. So a
-synthesizer will have an &ldquo;input&rdquo; port that receives messages. A keyboard will
-have an &ldquo;output&rdquo; port that sends messages.</p>
+<p>Note that &ldquo;input&rdquo; and &ldquo;output&rdquo; 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;
+ }
+}