summaryrefslogtreecommitdiff
path: root/android/media
diff options
context:
space:
mode:
Diffstat (limited to 'android/media')
-rw-r--r--android/media/AudioAttributes.java32
-rw-r--r--android/media/AudioDeviceInfo.java29
-rw-r--r--android/media/AudioFocusInfo.java21
-rw-r--r--android/media/AudioFormat.java12
-rw-r--r--android/media/AudioManager.java426
-rw-r--r--android/media/AudioManagerInternal.java2
-rw-r--r--android/media/AudioPresentation.java190
-rw-r--r--android/media/AudioRecord.java110
-rw-r--r--android/media/AudioSystem.java11
-rw-r--r--android/media/AudioTrack.java87
-rw-r--r--android/media/ClosedCaptionRenderer.java4
-rw-r--r--android/media/DataSourceDesc.java39
-rw-r--r--android/media/ExifInterface.java31
-rw-r--r--android/media/Image.java26
-rw-r--r--android/media/ImageReader.java31
-rw-r--r--android/media/ImageWriter.java18
-rw-r--r--android/media/Media2DataSource.java1
-rw-r--r--android/media/MediaActionSound.java27
-rw-r--r--android/media/MediaBrowser2.java184
-rw-r--r--android/media/MediaBrowser2Test.java141
-rw-r--r--android/media/MediaCodec.java125
-rw-r--r--android/media/MediaCodecInfo.java61
-rw-r--r--android/media/MediaController2.java565
-rw-r--r--android/media/MediaController2Test.java487
-rw-r--r--android/media/MediaDataSource.java4
-rw-r--r--android/media/MediaDescrambler.java46
-rw-r--r--android/media/MediaDrm.java541
-rw-r--r--android/media/MediaExtractor.java19
-rw-r--r--android/media/MediaFormat.java82
-rw-r--r--android/media/MediaItem2.java162
-rw-r--r--android/media/MediaLibraryService2.java438
-rw-r--r--android/media/MediaMetadata2.java591
-rw-r--r--android/media/MediaMetadataRetriever.java223
-rw-r--r--android/media/MediaMuxer.java1
-rw-r--r--android/media/MediaPlayer.java189
-rw-r--r--android/media/MediaPlayer2.java1500
-rw-r--r--android/media/MediaPlayer2Impl.java2611
-rw-r--r--android/media/MediaPlayerBase.java317
-rw-r--r--android/media/MediaPlaylistAgent.java357
-rw-r--r--android/media/MediaRecorder.java51
-rw-r--r--android/media/MediaScanner.java59
-rw-r--r--android/media/MediaSession2.java1525
-rw-r--r--android/media/MediaSession2Test.java273
-rw-r--r--android/media/MediaSession2TestBase.java210
-rw-r--r--android/media/MediaSessionManager_MediaSession2.java223
-rw-r--r--android/media/MediaSessionService2.java74
-rw-r--r--android/media/MediaTimestamp.java16
-rw-r--r--android/media/MicrophoneInfo.java398
-rw-r--r--android/media/MiniThumbFile.java54
-rw-r--r--android/media/MockMediaLibraryService2.java98
-rw-r--r--android/media/MockMediaSessionService2.java102
-rw-r--r--android/media/MockPlayer.java146
-rw-r--r--android/media/PlaybackListenerHolder.java73
-rw-r--r--android/media/PlaybackState2.java216
-rw-r--r--android/media/PlayerBase.java5
-rw-r--r--android/media/Rating2.java148
-rw-r--r--android/media/Ringtone.java39
-rw-r--r--android/media/RingtoneManager.java15
-rw-r--r--android/media/SessionCommand2.java336
-rw-r--r--android/media/SessionCommandGroup2.java106
-rw-r--r--android/media/SessionToken2.java187
-rw-r--r--android/media/SubtitleData.java78
-rw-r--r--android/media/TestServiceRegistry.java135
-rw-r--r--android/media/TestUtils.java124
-rw-r--r--android/media/VolumePolicy.java2
-rw-r--r--android/media/VolumeProvider2.java147
-rw-r--r--android/media/audiofx/AudioEffect.java44
-rw-r--r--android/media/audiofx/DynamicsProcessing.java2402
-rw-r--r--android/media/audiofx/Visualizer.java25
-rw-r--r--android/media/audiopolicy/AudioMix.java18
-rw-r--r--android/media/audiopolicy/AudioMixingRule.java31
-rw-r--r--android/media/audiopolicy/AudioPolicy.java152
-rw-r--r--android/media/audiopolicy/AudioPolicyConfig.java53
-rw-r--r--android/media/midi/MidiManager.java3
-rw-r--r--android/media/session/MediaController.java44
-rw-r--r--android/media/session/MediaSession.java295
-rw-r--r--android/media/session/MediaSessionManager.java273
-rw-r--r--android/media/soundtrigger/SoundTriggerDetectionService.java292
-rw-r--r--android/media/soundtrigger/SoundTriggerManager.java58
-rw-r--r--android/media/update/ApiLoader.java55
-rw-r--r--android/media/update/MediaBrowser2Provider.java11
-rw-r--r--android/media/update/MediaControlView2Provider.java29
-rw-r--r--android/media/update/MediaController2Provider.java40
-rw-r--r--android/media/update/MediaItem2Provider.java46
-rw-r--r--android/media/update/MediaLibraryService2Provider.java16
-rw-r--r--android/media/update/MediaMetadata2Provider.java38
-rw-r--r--android/media/update/MediaPlaylistAgentProvider.java55
-rw-r--r--android/media/update/MediaSession2Provider.java103
-rw-r--r--android/media/update/MediaSessionService2Provider.java9
-rw-r--r--android/media/update/ProviderCreator.java23
-rw-r--r--android/media/update/Rating2Provider.java36
-rw-r--r--android/media/update/SessionToken2Provider.java34
-rw-r--r--android/media/update/StaticProvider.java114
-rw-r--r--android/media/update/TransportControlProvider.java18
-rw-r--r--android/media/update/VideoView2Provider.java72
-rw-r--r--android/media/update/ViewGroupHelper.java369
-rw-r--r--android/media/update/ViewGroupProvider.java (renamed from android/media/update/ViewProvider.java)32
-rw-r--r--android/media/update/VolumeProvider2Provider.java26
98 files changed, 12786 insertions, 6611 deletions
diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java
index 44a2ff9e..d4326583 100644
--- a/android/media/AudioAttributes.java
+++ b/android/media/AudioAttributes.java
@@ -19,7 +19,6 @@ 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;
@@ -206,19 +205,26 @@ public final class AudioAttributes implements Parcelable {
/**
* @hide
* Denotes a usage for alarms,
- * will be muted when the Zen mode doesn't allow alarms
+ * will be muted when the Zen mode priority doesn't allow alarms or in Alarms Only Mode
* @see #SUPPRESSIBLE_USAGES
*/
public final static int SUPPRESSIBLE_ALARM = 4;
/**
* @hide
+ * Denotes a usage for media, game, assistant, and navigation
+ * will be muted when the Zen priority mode doesn't allow media
+ * @see #SUPPRESSIBLE_USAGES
+ */
+ public final static int SUPPRESSIBLE_MEDIA = 5;
+ /**
+ * @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.
+ * SUPPRESSIBLE_CALL,SUPPRESSIBLE_NEVER, SUPPRESSIBLE_ALARM or SUPPRESSIBLE_MEDIA.
+ * This includes system, sonification and unknown sounds.
+ * These will be muted when the Zen priority mode doesn't allow sytem sounds
* @see #SUPPRESSIBLE_USAGES
*/
- public final static int SUPPRESSIBLE_MEDIA_SYSTEM_OTHER = 5;
+ public final static int SUPPRESSIBLE_SYSTEM = 6;
/**
* @hide
@@ -239,13 +245,13 @@ public final class AudioAttributes implements Parcelable {
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);
+ SUPPRESSIBLE_USAGES.put(USAGE_MEDIA, SUPPRESSIBLE_MEDIA);
+ SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, SUPPRESSIBLE_MEDIA);
+ SUPPRESSIBLE_USAGES.put(USAGE_GAME, SUPPRESSIBLE_MEDIA);
+ SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT, SUPPRESSIBLE_MEDIA);
+ SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING, SUPPRESSIBLE_SYSTEM);
+ SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION, SUPPRESSIBLE_SYSTEM);
+ SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN, SUPPRESSIBLE_SYSTEM);
}
/**
diff --git a/android/media/AudioDeviceInfo.java b/android/media/AudioDeviceInfo.java
index 1a97b6ba..ca895fcd 100644
--- a/android/media/AudioDeviceInfo.java
+++ b/android/media/AudioDeviceInfo.java
@@ -22,6 +22,7 @@ import android.util.SparseIntArray;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
import java.util.TreeSet;
/**
@@ -122,6 +123,10 @@ public final class AudioDeviceInfo {
* A device type describing a USB audio headset.
*/
public static final int TYPE_USB_HEADSET = 22;
+ /**
+ * A device type describing a Hearing Aid.
+ */
+ public static final int TYPE_HEARING_AID = 23;
/** @hide */
@IntDef(flag = false, prefix = "TYPE", value = {
@@ -142,7 +147,9 @@ public final class AudioDeviceInfo {
TYPE_LINE_DIGITAL,
TYPE_FM,
TYPE_AUX_LINE,
- TYPE_IP }
+ TYPE_IP,
+ TYPE_BUS,
+ TYPE_HEARING_AID }
)
@Retention(RetentionPolicy.SOURCE)
public @interface AudioDeviceTypeOut {}
@@ -168,12 +175,27 @@ public final class AudioDeviceInfo {
case TYPE_FM:
case TYPE_AUX_LINE:
case TYPE_IP:
+ case TYPE_BUS:
+ case TYPE_HEARING_AID:
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) {
@@ -204,11 +226,10 @@ public final class AudioDeviceInfo {
}
/**
- * @hide
* @return The "address" string of the device. This generally contains device-specific
* parameters.
*/
- public String getAddress() {
+ public @NonNull String getAddress() {
return mPort.address();
}
@@ -351,6 +372,7 @@ public final class AudioDeviceInfo {
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_AUX_LINE, TYPE_AUX_LINE);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_IP, TYPE_IP);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_BUS, TYPE_BUS);
+ INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_HEARING_AID, TYPE_HEARING_AID);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BUILTIN_MIC, TYPE_BUILTIN_MIC);
INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET, TYPE_BLUETOOTH_SCO);
@@ -399,6 +421,7 @@ public final class AudioDeviceInfo {
EXT_TO_INT_DEVICE_MAPPING.put(TYPE_AUX_LINE, AudioSystem.DEVICE_OUT_AUX_LINE);
EXT_TO_INT_DEVICE_MAPPING.put(TYPE_IP, AudioSystem.DEVICE_OUT_IP);
EXT_TO_INT_DEVICE_MAPPING.put(TYPE_BUS, AudioSystem.DEVICE_OUT_BUS);
+ EXT_TO_INT_DEVICE_MAPPING.put(TYPE_HEARING_AID, AudioSystem.DEVICE_OUT_HEARING_AID);
}
}
diff --git a/android/media/AudioFocusInfo.java b/android/media/AudioFocusInfo.java
index 5d0c8e23..5467a69e 100644
--- a/android/media/AudioFocusInfo.java
+++ b/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,6 +142,7 @@ public final class AudioFocusInfo implements Parcelable {
dest.writeInt(mLossReceived);
dest.writeInt(mFlags);
dest.writeInt(mSdkTarget);
+ dest.writeLong(mGenCount);
}
@Override
@@ -168,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;
}
@@ -175,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
@@ -185,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/android/media/AudioFormat.java b/android/media/AudioFormat.java
index b07d0422..f98480b2 100644
--- a/android/media/AudioFormat.java
+++ b/android/media/AudioFormat.java
@@ -265,6 +265,12 @@ public final class AudioFormat implements Parcelable {
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) {
@@ -512,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:
@@ -537,6 +544,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_IEC61937:
@@ -564,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:
@@ -593,6 +602,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:
@@ -829,6 +839,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_IEC61937:
@@ -1044,6 +1055,7 @@ public final class AudioFormat implements Parcelable {
ENCODING_PCM_FLOAT,
ENCODING_AC3,
ENCODING_E_AC3,
+ ENCODING_E_AC3_JOC,
ENCODING_DTS,
ENCODING_DTS_HD,
ENCODING_IEC61937,
diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java
index 2ac4063d..aeef2158 100644
--- a/android/media/AudioManager.java
+++ b/android/media/AudioManager.java
@@ -32,6 +32,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;
@@ -48,17 +49,25 @@ 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.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.
@@ -400,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";
@@ -1331,6 +1352,7 @@ public class AudioManager {
//====================================================================
// Offload query
/**
+ * @hide
* 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.
@@ -2077,27 +2099,7 @@ public class AudioManager {
*/
private boolean querySoundEffectsEnabled(int user) {
return Settings.System.getIntForUser(getContext().getContentResolver(),
- 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;
- }
+ Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0;
}
/**
@@ -2324,6 +2326,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) {
@@ -2376,6 +2392,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.
@@ -2642,18 +2692,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;
}
/**
@@ -2700,6 +2832,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.
@@ -2989,7 +3147,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 {
@@ -3736,6 +3894,21 @@ public class AudioManager {
}
/**
+ * Indicate Hearing Aid connection state change.
+ * @param device Bluetooth device connected/disconnected
+ * @param state new connection state (BluetoothProfile.STATE_xxx)
+ * {@hide}
+ */
+ public void setHearingAidDeviceConnectionState(BluetoothDevice device, int state) {
+ final IAudioService service = getService();
+ try {
+ service.setHearingAidDeviceConnectionState(device, state);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Indicate A2DP source or sink connection state change.
* @param device Bluetooth device connected/disconnected
* @param state new connection state (BluetoothProfile.STATE_xxx)
@@ -4423,8 +4596,7 @@ public class AudioManager {
private static boolean checkTypes(AudioDevicePort port) {
return AudioDeviceInfo.convertInternalDeviceToDeviceType(port.type()) !=
- AudioDeviceInfo.TYPE_UNKNOWN &&
- port.type() != AudioSystem.DEVICE_IN_BACK_MIC;
+ AudioDeviceInfo.TYPE_UNKNOWN;
}
/**
@@ -4566,6 +4738,82 @@ 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);
+ }
+ }
+ }
+
+ /**
+ * Convert {@link AudioDeviceInfo} to {@link MicrophoneInfo}.
+ * @hide
+ */
+ public static MicrophoneInfo microphoneInfoFromAudioDeviceInfo(AudioDeviceInfo deviceInfo) {
+ int deviceType = deviceInfo.getType();
+ int micLocation = (deviceType == AudioDeviceInfo.TYPE_BUILTIN_MIC
+ || deviceType == AudioDeviceInfo.TYPE_TELEPHONY) ? MicrophoneInfo.LOCATION_MAINBODY
+ : deviceType == AudioDeviceInfo.TYPE_UNKNOWN ? MicrophoneInfo.LOCATION_UNKNOWN
+ : MicrophoneInfo.LOCATION_PERIPHERAL;
+ MicrophoneInfo microphone = new MicrophoneInfo(
+ deviceInfo.getPort().name() + deviceInfo.getId(),
+ deviceInfo.getPort().type(), deviceInfo.getAddress(), micLocation,
+ MicrophoneInfo.GROUP_UNKNOWN, MicrophoneInfo.INDEX_IN_THE_GROUP_UNKNOWN,
+ MicrophoneInfo.POSITION_UNKNOWN, MicrophoneInfo.ORIENTATION_UNKNOWN,
+ new ArrayList<Pair<Float, Float>>(), new ArrayList<Pair<Integer, Integer>>(),
+ MicrophoneInfo.SENSITIVITY_UNKNOWN, MicrophoneInfo.SPL_UNKNOWN,
+ MicrophoneInfo.SPL_UNKNOWN, MicrophoneInfo.DIRECTIONALITY_UNKNOWN);
+ microphone.setId(deviceInfo.getId());
+ return microphone;
+ }
+
+ /**
+ * 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);
+ AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_INPUTS);
+ for (AudioDeviceInfo device : devices) {
+ if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC ||
+ device.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
+ continue;
+ }
+ MicrophoneInfo microphone = microphoneInfoFromAudioDeviceInfo(device);
+ microphones.add(microphone);
+ }
+ 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.
@@ -4645,6 +4893,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/android/media/AudioManagerInternal.java b/android/media/AudioManagerInternal.java
index 0a1de33b..98c2d7fd 100644
--- a/android/media/AudioManagerInternal.java
+++ b/android/media/AudioManagerInternal.java
@@ -42,6 +42,8 @@ public abstract class AudioManagerInternal {
public abstract void setRingerModeInternal(int ringerMode, String caller);
+ public abstract void silenceRingerModeInternal(String caller);
+
public abstract void updateRingerModeAffectedStreamsInternal();
public abstract void setAccessibilityServiceUids(IntArray uids);
diff --git a/android/media/AudioPresentation.java b/android/media/AudioPresentation.java
new file mode 100644
index 00000000..e39cb7db
--- /dev/null
+++ b/android/media/AudioPresentation.java
@@ -0,0 +1,190 @@
+/*
+ * 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.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+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;
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public AudioPresentation(int presentationId,
+ int programId,
+ @NonNull Map<String, String> labels,
+ @NonNull 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
+ */
+ @VisibleForTesting
+ 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
+ */
+ @VisibleForTesting
+ 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. 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/android/media/AudioRecord.java b/android/media/AudioRecord.java
index 27784e96..4f0dccb8 100644
--- a/android/media/AudioRecord.java
+++ b/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,10 +35,13 @@ 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;
+import android.util.Pair;
import com.android.internal.annotations.GuardedBy;
@@ -1314,6 +1320,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 +1417,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 +1427,7 @@ public class AudioRecord implements AudioRouting
/*
* Call AFTER removing a routing callback handler.
*/
+ @GuardedBy("mRoutingChangeListeners")
private void testDisableNativeRoutingCallbacksLocked() {
if (mRoutingChangeListeners.size() == 0) {
native_disableDeviceCallback();
@@ -1583,6 +1608,46 @@ 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);
+
+ // Use routed device when there is not information returned by hal.
+ if (activeMicrophones.size() == 0) {
+ AudioDeviceInfo device = getRoutedDevice();
+ if (device != null) {
+ MicrophoneInfo microphone = AudioManager.microphoneInfoFromAudioDeviceInfo(device);
+ ArrayList<Pair<Integer, Integer>> channelMapping = new ArrayList<>();
+ for (int i = 0; i < mChannelCount; i++) {
+ channelMapping.add(new Pair(i, MicrophoneInfo.CHANNEL_MAPPING_DIRECT));
+ }
+ microphone.setChannelMapping(channelMapping);
+ activeMicrophones.add(microphone);
+ }
+ }
+ return activeMicrophones;
+ }
+
//---------------------------------------------------------
// Interface definitions
//--------------------
@@ -1728,6 +1793,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
//------------------
@@ -1739,4 +1807,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/android/media/AudioSystem.java b/android/media/AudioSystem.java
index dcd37daf..aaba1e3c 100644
--- a/android/media/AudioSystem.java
+++ b/android/media/AudioSystem.java
@@ -401,6 +401,7 @@ public class AudioSystem
public static final int DEVICE_OUT_BUS = 0x1000000;
public static final int DEVICE_OUT_PROXY = 0x2000000;
public static final int DEVICE_OUT_USB_HEADSET = 0x4000000;
+ public static final int DEVICE_OUT_HEARING_AID = 0x8000000;
public static final int DEVICE_OUT_DEFAULT = DEVICE_BIT_DEFAULT;
@@ -431,6 +432,7 @@ public class AudioSystem
DEVICE_OUT_BUS |
DEVICE_OUT_PROXY |
DEVICE_OUT_USB_HEADSET |
+ DEVICE_OUT_HEARING_AID |
DEVICE_OUT_DEFAULT);
public static final int DEVICE_OUT_ALL_A2DP = (DEVICE_OUT_BLUETOOTH_A2DP |
DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES |
@@ -546,6 +548,7 @@ public class AudioSystem
public static final String DEVICE_OUT_BUS_NAME = "bus";
public static final String DEVICE_OUT_PROXY_NAME = "proxy";
public static final String DEVICE_OUT_USB_HEADSET_NAME = "usb_headset";
+ public static final String DEVICE_OUT_HEARING_AID_NAME = "hearing_aid_out";
public static final String DEVICE_IN_COMMUNICATION_NAME = "communication";
public static final String DEVICE_IN_AMBIENT_NAME = "ambient";
@@ -628,6 +631,8 @@ public class AudioSystem
return DEVICE_OUT_PROXY_NAME;
case DEVICE_OUT_USB_HEADSET:
return DEVICE_OUT_USB_HEADSET_NAME;
+ case DEVICE_OUT_HEARING_AID:
+ return DEVICE_OUT_HEARING_AID_NAME;
case DEVICE_OUT_DEFAULT:
default:
return Integer.toString(device);
@@ -742,7 +747,8 @@ public class AudioSystem
public static final int FOR_SYSTEM = 4;
public static final int FOR_HDMI_SYSTEM_AUDIO = 5;
public static final int FOR_ENCODED_SURROUND = 6;
- private static final int NUM_FORCE_USE = 7;
+ public static final int FOR_VIBRATE_RINGING = 7;
+ private static final int NUM_FORCE_USE = 8;
public static String forceUseUsageToString(int usage) {
switch (usage) {
@@ -753,6 +759,7 @@ public class AudioSystem
case FOR_SYSTEM: return "FOR_SYSTEM";
case FOR_HDMI_SYSTEM_AUDIO: return "FOR_HDMI_SYSTEM_AUDIO";
case FOR_ENCODED_SURROUND: return "FOR_ENCODED_SURROUND";
+ case FOR_VIBRATE_RINGING: return "FOR_VIBRATE_RINGING";
default: return "unknown usage (" + usage + ")" ;
}
}
@@ -827,6 +834,8 @@ public class AudioSystem
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
/**
diff --git a/android/media/AudioTrack.java b/android/media/AudioTrack.java
index 5928d03d..87b5d437 100644
--- a/android/media/AudioTrack.java
+++ b/android/media/AudioTrack.java
@@ -36,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;
@@ -895,6 +896,7 @@ public class AudioTrack extends PlayerBase
}
/**
+ * @hide
* 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
@@ -1718,6 +1720,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
//--------------------
@@ -1990,6 +2009,26 @@ 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.
+ * {@link #ERROR} is returned in case of any other error.
+ * @param presentation see {@link AudioPresentation}. In particular, id should be set.
+ * @return error code or success, see {@link #SUCCESS}, {@link #ERROR},
+ * {@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.
@@ -2784,6 +2823,7 @@ public class AudioTrack extends PlayerBase
/*
* Call BEFORE adding a routing callback handler.
*/
+ @GuardedBy("mRoutingChangeListeners")
private void testEnableNativeRoutingCallbacksLocked() {
if (mRoutingChangeListeners.size() == 0) {
native_enableDeviceCallback();
@@ -2793,6 +2833,7 @@ public class AudioTrack extends PlayerBase
/*
* Call AFTER removing a routing callback handler.
*/
+ @GuardedBy("mRoutingChangeListeners")
private void testDisableNativeRoutingCallbacksLocked() {
if (mRoutingChangeListeners.size() == 0) {
native_disableDeviceCallback();
@@ -2939,6 +2980,7 @@ public class AudioTrack extends PlayerBase
}
/**
+ * @hide
* 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.
@@ -2972,6 +3014,7 @@ public class AudioTrack extends PlayerBase
private final Object mStreamEventCbLock = new Object();
/**
+ * @hide
* 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
@@ -2991,6 +3034,7 @@ public class AudioTrack extends PlayerBase
}
/**
+ * @hide
* Unregisters the callback for notification of stream events, previously set
* by {@link #setStreamEventCallback(Executor, StreamEventCallback)}.
*/
@@ -3227,6 +3271,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
@@ -3239,4 +3284,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/android/media/ClosedCaptionRenderer.java b/android/media/ClosedCaptionRenderer.java
index cc7722a0..66759e53 100644
--- a/android/media/ClosedCaptionRenderer.java
+++ b/android/media/ClosedCaptionRenderer.java
@@ -59,7 +59,7 @@ public class ClosedCaptionRenderer extends SubtitleController.Renderer {
public boolean supports(MediaFormat format) {
if (format.containsKey(MediaFormat.KEY_MIME)) {
String mimeType = format.getString(MediaFormat.KEY_MIME);
- return MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608.equals(mimeType);
+ return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
}
return false;
}
@@ -67,7 +67,7 @@ public class ClosedCaptionRenderer extends SubtitleController.Renderer {
@Override
public SubtitleTrack createTrack(MediaFormat format) {
String mimeType = format.getString(MediaFormat.KEY_MIME);
- if (MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
+ if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
if (mCCWidget == null) {
mCCWidget = new Cea608CCWidget(mContext);
}
diff --git a/android/media/DataSourceDesc.java b/android/media/DataSourceDesc.java
index 73fad7ad..a53fa11f 100644
--- a/android/media/DataSourceDesc.java
+++ b/android/media/DataSourceDesc.java
@@ -30,6 +30,8 @@ import com.android.internal.util.Preconditions;
import java.io.FileDescriptor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.net.CookieHandler;
+import java.net.CookieManager;
import java.net.HttpCookie;
import java.util.ArrayList;
@@ -38,6 +40,7 @@ import java.util.List;
import java.util.Map;
/**
+ * @hide
* Structure for data source descriptor.
*
* Used by {@link MediaPlayer2#setDataSource(DataSourceDesc)}
@@ -72,7 +75,7 @@ public final class DataSourceDesc {
private List<HttpCookie> mUriCookies;
private Context mUriContext;
- private long mId = 0;
+ private String mMediaId;
private long mStartPositionMs = 0;
private long mEndPositionMs = LONG_MAX;
@@ -80,11 +83,11 @@ public final class DataSourceDesc {
}
/**
- * Return the Id of data source.
- * @return the Id of data source
+ * Return the media Id of data source.
+ * @return the media Id of data source
*/
- public long getId() {
- return mId;
+ public String getMediaId() {
+ return mMediaId;
}
/**
@@ -220,7 +223,7 @@ public final class DataSourceDesc {
private List<HttpCookie> mUriCookies;
private Context mUriContext;
- private long mId = 0;
+ private String mMediaId;
private long mStartPositionMs = 0;
private long mEndPositionMs = LONG_MAX;
@@ -246,7 +249,7 @@ public final class DataSourceDesc {
mUriCookies = dsd.mUriCookies;
mUriContext = dsd.mUriContext;
- mId = dsd.mId;
+ mMediaId = dsd.mMediaId;
mStartPositionMs = dsd.mStartPositionMs;
mEndPositionMs = dsd.mEndPositionMs;
}
@@ -280,7 +283,7 @@ public final class DataSourceDesc {
dsd.mUriCookies = mUriCookies;
dsd.mUriContext = mUriContext;
- dsd.mId = mId;
+ dsd.mMediaId = mMediaId;
dsd.mStartPositionMs = mStartPositionMs;
dsd.mEndPositionMs = mEndPositionMs;
@@ -288,13 +291,13 @@ public final class DataSourceDesc {
}
/**
- * Sets the Id of this data source.
+ * Sets the media Id of this data source.
*
- * @param id the Id of this data source
+ * @param mediaId the media Id of this data source
* @return the same Builder instance.
*/
- public Builder setId(long id) {
- mId = id;
+ public Builder setMediaId(String mediaId) {
+ mMediaId = mediaId;
return this;
}
@@ -433,10 +436,22 @@ public final class DataSourceDesc {
* @param cookies the cookies to be sent together with the request
* @return the same Builder instance.
* @throws NullPointerException if context or uri is null.
+ * @throws IllegalArgumentException if the cookie handler is not of CookieManager type
+ * when cookies are provided.
*/
public Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
@Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
+ Preconditions.checkNotNull(context, "context cannot be null");
Preconditions.checkNotNull(uri);
+ 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.");
+ }
+ }
+
resetDataSource();
mType = TYPE_URI;
mUri = uri;
diff --git a/android/media/ExifInterface.java b/android/media/ExifInterface.java
index 91754162..bc0e43b5 100644
--- a/android/media/ExifInterface.java
+++ b/android/media/ExifInterface.java
@@ -3226,9 +3226,18 @@ public class ExifInterface {
if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) {
long[] stripOffsets =
- (long[]) stripOffsetsAttribute.getValue(mExifByteOrder);
+ convertToLongArray(stripOffsetsAttribute.getValue(mExifByteOrder));
long[] stripByteCounts =
- (long[]) stripByteCountsAttribute.getValue(mExifByteOrder);
+ convertToLongArray(stripByteCountsAttribute.getValue(mExifByteOrder));
+
+ if (stripOffsets == null) {
+ Log.w(TAG, "stripOffsets should not be null.");
+ return;
+ }
+ if (stripByteCounts == null) {
+ Log.w(TAG, "stripByteCounts should not be null.");
+ return;
+ }
// Set thumbnail byte array data for non-consecutive strip bytes
byte[] totalStripBytes =
@@ -4025,4 +4034,22 @@ public class ExifInterface {
}
return false;
}
+
+ /**
+ * Convert given int[] to long[]. If long[] is given, just return it.
+ * Return null for other types of input.
+ */
+ private static long[] convertToLongArray(Object inputObj) {
+ if (inputObj instanceof int[]) {
+ int[] input = (int[]) inputObj;
+ long[] result = new long[input.length];
+ for (int i = 0; i < input.length; i++) {
+ result[i] = input[i];
+ }
+ return result;
+ } else if (inputObj instanceof long[]) {
+ return (long[]) inputObj;
+ }
+ return null;
+ }
}
diff --git a/android/media/Image.java b/android/media/Image.java
index fbe55614..37c57854 100644
--- a/android/media/Image.java
+++ b/android/media/Image.java
@@ -19,7 +19,9 @@ package android.media;
import java.nio.ByteBuffer;
import java.lang.AutoCloseable;
+import android.annotation.Nullable;
import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
/**
* <p>A single complete image buffer to use with a media source such as a
@@ -184,6 +186,30 @@ public abstract class Image implements AutoCloseable {
public abstract long getTimestamp();
/**
+ * Get the transformation associated with this frame.
+ * @return The window transformation that needs to be applied for this frame.
+ * @hide
+ */
+ public abstract int getTransform();
+
+ /**
+ * Get the {@link android.hardware.HardwareBuffer HardwareBuffer} handle of the input image
+ * intended for GPU and/or hardware access.
+ * <p>
+ * The returned {@link android.hardware.HardwareBuffer HardwareBuffer} shall not be used
+ * after {@link Image#close Image.close()} has been called.
+ * </p>
+ * @return the HardwareBuffer associated with this Image or null if this Image doesn't support
+ * this feature (e.g. {@link android.media.ImageWriter ImageWriter} or
+ * {@link android.media.MediaCodec MediaCodec} don't).
+ */
+ @Nullable
+ public HardwareBuffer getHardwareBuffer() {
+ throwISEIfImageIsInvalid();
+ return null;
+ }
+
+ /**
* Set the timestamp associated with this frame.
* <p>
* The timestamp is measured in nanoseconds, and is normally monotonically
diff --git a/android/media/ImageReader.java b/android/media/ImageReader.java
index 10195805..72d52d3d 100644
--- a/android/media/ImageReader.java
+++ b/android/media/ImageReader.java
@@ -727,18 +727,7 @@ public class ImageReader implements AutoCloseable {
return false;
}
- if (format == ImageFormat.PRIVATE) {
- // Usage need to be either USAGE0_GPU_SAMPLED_IMAGE or USAGE0_VIDEO_ENCODE or combined.
- boolean isAllowed = (usage == HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
- isAllowed = isAllowed || (usage == HardwareBuffer.USAGE_VIDEO_ENCODE);
- isAllowed = isAllowed || (usage ==
- (HardwareBuffer.USAGE_VIDEO_ENCODE | HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE));
- return isAllowed;
- } else {
- // Usage need to make the buffer CPU readable for explicit format.
- return ((usage == HardwareBuffer.USAGE_CPU_READ_RARELY) ||
- (usage == HardwareBuffer.USAGE_CPU_READ_OFTEN));
- }
+ return true;
}
/**
@@ -876,6 +865,18 @@ public class ImageReader implements AutoCloseable {
}
@Override
+ public int getTransform() {
+ throwISEIfImageIsInvalid();
+ return mTransform;
+ }
+
+ @Override
+ public HardwareBuffer getHardwareBuffer() {
+ throwISEIfImageIsInvalid();
+ return nativeGetHardwareBuffer();
+ }
+
+ @Override
public void setTimestamp(long timestampNs) {
throwISEIfImageIsInvalid();
mTimestamp = timestampNs;
@@ -1007,6 +1008,11 @@ public class ImageReader implements AutoCloseable {
*/
private long mTimestamp;
+ /**
+ * This field is set by native code during nativeImageSetup().
+ */
+ private int mTransform;
+
private SurfacePlane[] mPlanes;
private int mFormat = ImageFormat.UNKNOWN;
// If this image is detached from the ImageReader.
@@ -1017,6 +1023,7 @@ public class ImageReader implements AutoCloseable {
private synchronized native int nativeGetWidth();
private synchronized native int nativeGetHeight();
private synchronized native int nativeGetFormat(int readerFormat);
+ private synchronized native HardwareBuffer nativeGetHardwareBuffer();
}
private synchronized native void nativeInit(Object weakSelf, int w, int h,
diff --git a/android/media/ImageWriter.java b/android/media/ImageWriter.java
index 2b7309f1..8ee27ae5 100644
--- a/android/media/ImageWriter.java
+++ b/android/media/ImageWriter.java
@@ -371,7 +371,7 @@ public class ImageWriter implements AutoCloseable {
Rect crop = image.getCropRect();
nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), crop.left, crop.top,
- crop.right, crop.bottom);
+ crop.right, crop.bottom, image.getTransform());
/**
* Only remove and cleanup the Images that are owned by this
@@ -557,7 +557,8 @@ public class ImageWriter implements AutoCloseable {
// buffer caused leak.
Rect crop = image.getCropRect();
nativeAttachAndQueueImage(mNativeContext, image.getNativeContext(), image.getFormat(),
- image.getTimestamp(), crop.left, crop.top, crop.right, crop.bottom);
+ image.getTimestamp(), crop.left, crop.top, crop.right, crop.bottom,
+ image.getTransform());
}
/**
@@ -674,6 +675,8 @@ public class ImageWriter implements AutoCloseable {
private final long DEFAULT_TIMESTAMP = Long.MIN_VALUE;
private long mTimestamp = DEFAULT_TIMESTAMP;
+ private int mTransform = 0; //Default no transform
+
public WriterSurfaceImage(ImageWriter writer) {
mOwner = writer;
}
@@ -711,6 +714,13 @@ public class ImageWriter implements AutoCloseable {
}
@Override
+ public int getTransform() {
+ throwISEIfImageIsInvalid();
+
+ return mTransform;
+ }
+
+ @Override
public long getTimestamp() {
throwISEIfImageIsInvalid();
@@ -856,11 +866,11 @@ public class ImageWriter implements AutoCloseable {
private synchronized native void nativeDequeueInputImage(long nativeCtx, Image wi);
private synchronized native void nativeQueueInputImage(long nativeCtx, Image image,
- long timestampNs, int left, int top, int right, int bottom);
+ long timestampNs, int left, int top, int right, int bottom, int transform);
private synchronized native int nativeAttachAndQueueImage(long nativeCtx,
long imageNativeBuffer, int imageFormat, long timestampNs, int left,
- int top, int right, int bottom);
+ int top, int right, int bottom, int transform);
private synchronized native void cancelImage(long nativeCtx, Image image);
diff --git a/android/media/Media2DataSource.java b/android/media/Media2DataSource.java
index 8ee4a705..08df632a 100644
--- a/android/media/Media2DataSource.java
+++ b/android/media/Media2DataSource.java
@@ -21,6 +21,7 @@ import java.io.Closeable;
import java.io.IOException;
/**
+ * @hide
* For supplying media data to the framework. Implement this if your app has
* special requirements for the way media data is obtained.
*
diff --git a/android/media/MediaActionSound.java b/android/media/MediaActionSound.java
index 983ca754..dcd4dce5 100644
--- a/android/media/MediaActionSound.java
+++ b/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/android/media/MediaBrowser2.java b/android/media/MediaBrowser2.java
index be4be3fc..452371a4 100644
--- a/android/media/MediaBrowser2.java
+++ b/android/media/MediaBrowser2.java
@@ -16,9 +16,12 @@
package android.media;
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaSession2.ControllerInfo;
import android.media.update.ApiLoader;
import android.media.update.MediaBrowser2Provider;
import android.os.Bundle;
@@ -27,8 +30,8 @@ import java.util.List;
import java.util.concurrent.Executor;
/**
- * Browses media content offered by a {@link MediaLibraryService2}.
* @hide
+ * Browses media content offered by a {@link MediaLibraryService2}.
*/
public class MediaBrowser2 extends MediaController2 {
// Equals to the ((MediaBrowser2Provider) getProvider())
@@ -39,138 +42,197 @@ public class MediaBrowser2 extends MediaController2 {
*/
public static class BrowserCallback extends MediaController2.ControllerCallback {
/**
- * Called with the result of {@link #getBrowserRoot(Bundle)}.
+ * Called with the result of {@link #getLibraryRoot(Bundle)}.
* <p>
- * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the browser root isn't
+ * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the library root isn't
* available.
*
+ * @param browser the browser for this event
* @param rootHints rootHints that you previously requested.
- * @param rootMediaId media id of the browser root. Can be {@code null}
- * @param rootExtra extra of the browser root. Can be {@code null}
+ * @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) { }
+ public void onGetLibraryRootDone(@NonNull MediaBrowser2 browser, @Nullable Bundle rootHints,
+ @Nullable String rootMediaId, @Nullable Bundle rootExtra) { }
/**
- * Called when the item has been returned by the library service for the previous
- * {@link MediaBrowser2#getItem} call.
+ * Called when there's change in the parent's children.
* <p>
- * Result can be null if there had been error.
+ * This API is called when the library service called
+ * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)} or
+ * {@link MediaLibrarySession#notifyChildrenChanged(String, int, Bundle)} for the parent.
*
- * @param mediaId media id
- * @param result result. Can be {@code null}
+ * @param browser the browser for this event
+ * @param parentId parent id that you've specified with {@link #subscribe(String, Bundle)}
+ * @param itemCount number of children
+ * @param extras extra bundle from the library service. Can be differ from extras that
+ * you've specified with {@link #subscribe(String, Bundle)}.
*/
- public void onItemLoaded(@NonNull String mediaId, @Nullable MediaItem2 result) { }
+ public void onChildrenChanged(@NonNull MediaBrowser2 browser, @NonNull String parentId,
+ int itemCount, @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 browser the browser for this event
* @param parentId parent id
- * @param page page number that you've specified
- * @param pageSize page size that you've specified
- * @param options optional bundle that you've specified
+ * @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 result result. Can be {@code null}
+ * @param extras extra bundle from the library service
*/
- public void onChildrenLoaded(@NonNull String parentId, int page, int pageSize,
- @Nullable Bundle options, @Nullable List<MediaItem2> result) { }
+ public void onGetChildrenDone(@NonNull MediaBrowser2 browser, @NonNull String parentId,
+ int page, int pageSize, @Nullable List<MediaItem2> result,
+ @Nullable Bundle extras) { }
/**
- * Called when there's change in the parent's children.
+ * 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 browser the browser for this event
+ * @param mediaId media id
+ * @param result result. Can be {@code null}
+ */
+ public void onGetItemDone(@NonNull MediaBrowser2 browser, @NonNull String mediaId,
+ @Nullable MediaItem2 result) { }
+
+ /**
+ * Called when there's change in the search result requested by the previous
+ * {@link MediaBrowser2#search(String, Bundle)}.
*
- * @param parentId parent id that you've specified with subscribe
- * @param options optional bundle that you've specified with subscribe
+ * @param browser the browser for this event
+ * @param query search query that you've specified with {@link #search(String, Bundle)}
+ * @param itemCount The item count for the search result
+ * @param extras extra bundle from the library service
*/
- public void onChildrenChanged(@NonNull String parentId, @Nullable Bundle options) { }
+ public void onSearchResultChanged(@NonNull MediaBrowser2 browser, @NonNull String query,
+ int itemCount, @Nullable Bundle extras) { }
/**
* Called when the search result has been returned by the library service for the previous
- * {@link MediaBrowser2#search(String, int, int, Bundle)}.
+ * {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
* <p>
* Result can be null if there had been error.
*
- * @param query query string that you've specified
- * @param page page number that you've specified
- * @param pageSize page size that you've specified
- * @param options optional bundle that you've specified
- * @param result result. Can be {@code null}
+ * @param browser the browser for this event
+ * @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 result result. Can be {@code null}.
+ * @param extras extra bundle from the library service
*/
- public void onSearchResult(@NonNull String query, int page, int pageSize,
- @Nullable Bundle options, @Nullable List<MediaItem2> result) { }
+ public void onGetSearchResultDone(@NonNull MediaBrowser2 browser, @NonNull String query,
+ int page, int pageSize, @Nullable List<MediaItem2> result,
+ @Nullable Bundle extras) { }
}
- public MediaBrowser2(Context context, SessionToken2 token, BrowserCallback callback,
- Executor executor) {
- super(context, token, callback, executor);
+ 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,
- ControllerCallback callback, Executor executor) {
- return ApiLoader.getProvider(context)
- .createMediaBrowser2(this, context, token, (BrowserCallback) callback, executor);
+ Executor executor, ControllerCallback callback) {
+ return ApiLoader.getProvider().createMediaBrowser2(
+ context, this, token, executor, (BrowserCallback) callback);
}
- public void getBrowserRoot(Bundle rootHints) {
- mProvider.getBrowserRoot_impl(rootHints);
+ /**
+ * Get the library root. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)}.
+ *
+ * @param rootHints hint for the root
+ * @see BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)
+ */
+ public void getLibraryRoot(@Nullable 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.
+ * {@link BrowserCallback#onChildrenChanged(MediaBrowser2, String, int, 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 options optional bundle
+ * @param extras extra bundle
*/
- public void subscribe(String parentId, @Nullable Bundle options) {
- mProvider.subscribe_impl(parentId, options);
+ public void subscribe(@NonNull 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)}.
+ * <p>
+ * This unsubscribes all previous subscription with the parent id, regardless of the extra
+ * that was previously sent to the library service.
*
* @param parentId parent id
- * @param options optional bundle
*/
- public void unsubscribe(String parentId, @Nullable Bundle options) {
- mProvider.unsubscribe_impl(parentId, options);
+ public void unsubscribe(@NonNull String parentId) {
+ mProvider.unsubscribe_impl(parentId);
+ }
+
+ /**
+ * Get list of children under the parent. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onGetChildrenDone(MediaBrowser2, String, int, int, List, Bundle)}.
+ *
+ * @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(@NonNull 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)}.
+ * {@link BrowserCallback#onGetItemDone(MediaBrowser2, String, MediaItem2)}.
*
- * @param mediaId media id
+ * @param mediaId media id for specifying the item
*/
- public void getItem(String mediaId) {
+ public void getItem(@NonNull String mediaId) {
mProvider.getItem_impl(mediaId);
}
/**
- * Get list of children under the parent. Result would be sent back asynchronously with the
- * {@link BrowserCallback#onChildrenLoaded(String, int, int, Bundle, List)}.
+ * Send a search request to the library service. When the search result is changed,
+ * {@link BrowserCallback#onSearchResultChanged(MediaBrowser2, String, int, Bundle)} will be
+ * called. You should call {@link #getSearchResult(String, int, int, Bundle)} to get the actual
+ * search result.
*
- * @param parentId
- * @param page
- * @param pageSize
- * @param options
+ * @param query search query. Should not be an empty string.
+ * @param extras extra bundle
*/
- public void getChildren(String parentId, int page, int pageSize, @Nullable Bundle options) {
- mProvider.getChildren_impl(parentId, page, pageSize, options);
+ 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#onGetSearchResultDone(MediaBrowser2, String, int, int, List, Bundle)}.
*
- * @param query search query deliminated by string
+ * @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 search(String query, int page, int pageSize, Bundle extras) {
- mProvider.search_impl(query, page, pageSize, extras);
+ public void getSearchResult(@NonNull String query, int page, int pageSize,
+ @Nullable Bundle extras) {
+ mProvider.getSearchResult_impl(query, page, pageSize, extras);
}
}
diff --git a/android/media/MediaBrowser2Test.java b/android/media/MediaBrowser2Test.java
deleted file mode 100644
index 5c960c85..00000000
--- a/android/media/MediaBrowser2Test.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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 static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import android.content.Context;
-import android.media.MediaBrowser2.BrowserCallback;
-import android.media.MediaSession2.CommandGroup;
-import android.os.Bundle;
-import android.support.annotation.CallSuper;
-import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Tests {@link MediaBrowser2}.
- * <p>
- * This test inherits {@link MediaController2Test} to ensure that inherited APIs from
- * {@link MediaController2} works cleanly.
- */
-// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class MediaBrowser2Test extends MediaController2Test {
- private static final String TAG = "MediaBrowser2Test";
-
- @Override
- TestControllerInterface onCreateController(@NonNull SessionToken2 token,
- @NonNull TestControllerCallbackInterface callback) {
- return new TestMediaBrowser(mContext, token, new TestBrowserCallback(callback));
- }
-
- @Test
- public void testGetBrowserRoot() throws InterruptedException {
- final Bundle param = new Bundle();
- param.putString(TAG, TAG);
-
- final CountDownLatch latch = new CountDownLatch(1);
- final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
- @Override
- public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
- assertTrue(TestUtils.equals(param, rootHints));
- assertEquals(MockMediaLibraryService2.ROOT_ID, rootMediaId);
- assertTrue(TestUtils.equals(MockMediaLibraryService2.EXTRA, rootExtra));
- latch.countDown();
- }
- };
-
- final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
- MediaBrowser2 browser =
- (MediaBrowser2) createController(token,true, callback);
- browser.getBrowserRoot(param);
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- }
-
- public static class TestBrowserCallback extends BrowserCallback
- implements WaitForConnectionInterface {
- private final TestControllerCallbackInterface mCallbackProxy;
- public final CountDownLatch connectLatch = new CountDownLatch(1);
- public final CountDownLatch disconnectLatch = new CountDownLatch(1);
-
- TestBrowserCallback(TestControllerCallbackInterface callbackProxy) {
- mCallbackProxy = callbackProxy;
- }
-
- @CallSuper
- @Override
- public void onConnected(CommandGroup commands) {
- super.onConnected(commands);
- connectLatch.countDown();
- }
-
- @CallSuper
- @Override
- public void onDisconnected() {
- super.onDisconnected();
- disconnectLatch.countDown();
- }
-
- @Override
- public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
- mCallbackProxy.onGetRootResult(rootHints, rootMediaId, rootExtra);
- }
-
- @Override
- public void waitForConnect(boolean expect) throws InterruptedException {
- if (expect) {
- assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } else {
- assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
- }
-
- @Override
- public void waitForDisconnect(boolean expect) throws InterruptedException {
- if (expect) {
- assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } else {
- assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
- }
- }
-
- public class TestMediaBrowser extends MediaBrowser2 implements TestControllerInterface {
- private final BrowserCallback mCallback;
-
- public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
- @NonNull ControllerCallback callback) {
- super(context, token, (BrowserCallback) callback, sHandlerExecutor);
- mCallback = (BrowserCallback) callback;
- }
-
- @Override
- public BrowserCallback getCallback() {
- return mCallback;
- }
- }
-} \ No newline at end of file
diff --git a/android/media/MediaCodec.java b/android/media/MediaCodec.java
index 3d5f6bc9..e3fba0cd 100644
--- a/android/media/MediaCodec.java
+++ b/android/media/MediaCodec.java
@@ -1602,7 +1602,9 @@ final public class MediaCodec {
private EventHandler mCallbackHandler;
private Callback mCallback;
private OnFrameRenderedListener mOnFrameRenderedListener;
- private Object mListenerLock = new Object();
+ private final Object mListenerLock = new Object();
+ private MediaCodecInfo mCodecInfo;
+ private final Object mCodecInfoLock = new Object();
private static final int EVENT_CALLBACK = 1;
private static final int EVENT_SET_CALLBACK = 2;
@@ -2357,17 +2359,61 @@ final public class MediaCodec {
public static final int CRYPTO_MODE_AES_CBC = 2;
/**
- * Metadata describing the structure of a (at least partially) encrypted
- * input sample.
- * A buffer's data is considered to be partitioned into "subSamples",
- * each subSample starts with a (potentially empty) run of plain,
- * unencrypted bytes followed by a (also potentially empty) run of
- * encrypted bytes. If pattern encryption applies, each of the latter runs
- * is encrypted only partly, according to a repeating pattern of "encrypt"
- * and "skip" blocks. numBytesOfClearData can be null to indicate that all
- * data is encrypted. This information encapsulates per-sample metadata as
- * outlined in ISO/IEC FDIS 23001-7:2011 "Common encryption in ISO base
- * media file format files".
+ * Metadata describing the structure of an encrypted input sample.
+ * <p>
+ * A buffer's data is considered to be partitioned into "subSamples". Each subSample starts with
+ * a run of plain, unencrypted bytes followed by a run of encrypted bytes. Either of these runs
+ * may be empty. If pattern encryption applies, each of the encrypted runs is encrypted only
+ * partly, according to a repeating pattern of "encrypt" and "skip" blocks.
+ * {@link #numBytesOfClearData} can be null to indicate that all data is encrypted, and
+ * {@link #numBytesOfEncryptedData} can be null to indicate that all data is clear. At least one
+ * of {@link #numBytesOfClearData} and {@link #numBytesOfEncryptedData} must be non-null.
+ * <p>
+ * This information encapsulates per-sample metadata as outlined in ISO/IEC FDIS 23001-7:2016
+ * "Common encryption in ISO base media file format files".
+ * <p>
+ * <h3>ISO-CENC Schemes</h3>
+ * ISO/IEC FDIS 23001-7:2016 defines four possible schemes by which media may be encrypted,
+ * corresponding to each possible combination of an AES mode with the presence or absence of
+ * patterned encryption.
+ *
+ * <table style="width: 0%">
+ * <thead>
+ * <tr>
+ * <th>&nbsp;</th>
+ * <th>AES-CTR</th>
+ * <th>AES-CBC</th>
+ * </tr>
+ * </thead>
+ * <tbody>
+ * <tr>
+ * <th>Without Patterns</th>
+ * <td>cenc</td>
+ * <td>cbc1</td>
+ * </tr><tr>
+ * <th>With Patterns</th>
+ * <td>cens</td>
+ * <td>cbcs</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ *
+ * For {@code CryptoInfo}, the scheme is selected implicitly by the combination of the
+ * {@link #mode} field and the value set with {@link #setPattern}. For the pattern, setting the
+ * pattern to all zeroes (that is, both {@code blocksToEncrypt} and {@code blocksToSkip} are
+ * zero) is interpreted as turning patterns off completely. A scheme that does not use patterns
+ * will be selected, either cenc or cbc1. Setting the pattern to any nonzero value will choose
+ * one of the pattern-supporting schemes, cens or cbcs. The default pattern if
+ * {@link #setPattern} is never called is all zeroes.
+ * <p>
+ * <h4>HLS SAMPLE-AES Audio</h4>
+ * HLS SAMPLE-AES audio is encrypted in a manner compatible with the cbcs scheme, except that it
+ * does not use patterned encryption. However, if {@link #setPattern} is used to set the pattern
+ * to all zeroes, this will be interpreted as selecting the cbc1 scheme. The cbc1 scheme cannot
+ * successfully decrypt HLS SAMPLE-AES audio because of differences in how the IVs are handled.
+ * For this reason, it is recommended that a pattern of {@code 1} encrypted block and {@code 0}
+ * skip blocks be used with HLS SAMPLE-AES audio. This will trigger decryption to use cbcs mode
+ * while still decrypting every block.
*/
public final static class CryptoInfo {
/**
@@ -2375,11 +2421,13 @@ final public class MediaCodec {
*/
public int numSubSamples;
/**
- * The number of leading unencrypted bytes in each subSample.
+ * The number of leading unencrypted bytes in each subSample. If null, all bytes are treated
+ * as encrypted and {@link #numBytesOfEncryptedData} must be specified.
*/
public int[] numBytesOfClearData;
/**
- * The number of trailing encrypted bytes in each subSample.
+ * The number of trailing encrypted bytes in each subSample. If null, all bytes are treated
+ * as clear and {@link #numBytesOfClearData} must be specified.
*/
public int[] numBytesOfEncryptedData;
/**
@@ -2398,35 +2446,34 @@ final public class MediaCodec {
public int mode;
/**
- * Metadata describing an encryption pattern for the protected bytes in
- * a subsample. An encryption pattern consists of a repeating sequence
- * of crypto blocks comprised of a number of encrypted blocks followed
- * by a number of unencrypted, or skipped, blocks.
+ * Metadata describing an encryption pattern for the protected bytes in a subsample. An
+ * encryption pattern consists of a repeating sequence of crypto blocks comprised of a
+ * number of encrypted blocks followed by a number of unencrypted, or skipped, blocks.
*/
public final static class Pattern {
/**
- * Number of blocks to be encrypted in the pattern. If zero, pattern
- * encryption is inoperative.
+ * Number of blocks to be encrypted in the pattern. If both this and
+ * {@link #mSkipBlocks} are zero, pattern encryption is inoperative.
*/
private int mEncryptBlocks;
/**
- * Number of blocks to be skipped (left clear) in the pattern. If zero,
- * pattern encryption is inoperative.
+ * Number of blocks to be skipped (left clear) in the pattern. If both this and
+ * {@link #mEncryptBlocks} are zero, pattern encryption is inoperative.
*/
private int mSkipBlocks;
/**
- * Construct a sample encryption pattern given the number of blocks to
- * encrypt and skip in the pattern.
+ * Construct a sample encryption pattern given the number of blocks to encrypt and skip
+ * in the pattern. If both parameters are zero, pattern encryption is inoperative.
*/
public Pattern(int blocksToEncrypt, int blocksToSkip) {
set(blocksToEncrypt, blocksToSkip);
}
/**
- * Set the number of blocks to encrypt and skip in a sample encryption
- * pattern.
+ * Set the number of blocks to encrypt and skip in a sample encryption pattern. If both
+ * parameters are zero, pattern encryption is inoperative.
*/
public void set(int blocksToEncrypt, int blocksToSkip) {
mEncryptBlocks = blocksToEncrypt;
@@ -3469,10 +3516,26 @@ final public class MediaCodec {
*/
@NonNull
public MediaCodecInfo getCodecInfo() {
- return MediaCodecList.getInfoFor(getName());
+ // Get the codec name first. If the codec is already released,
+ // IllegalStateException will be thrown here.
+ String name = getName();
+ synchronized (mCodecInfoLock) {
+ if (mCodecInfo == null) {
+ // Get the codec info for this codec itself first. Only initialize
+ // the full codec list if this somehow fails because it can be slow.
+ mCodecInfo = getOwnCodecInfo();
+ if (mCodecInfo == null) {
+ mCodecInfo = MediaCodecList.getInfoFor(name);
+ }
+ }
+ return mCodecInfo;
+ }
}
@NonNull
+ private native final MediaCodecInfo getOwnCodecInfo();
+
+ @NonNull
private native final ByteBuffer[] getBuffers(boolean input);
@Nullable
@@ -3510,6 +3573,8 @@ final public class MediaCodec {
private final static int TYPE_YUV = 1;
+ private final int mTransform = 0; //Default no transform
+
@Override
public int getFormat() {
throwISEIfImageIsInvalid();
@@ -3529,6 +3594,12 @@ final public class MediaCodec {
}
@Override
+ public int getTransform() {
+ throwISEIfImageIsInvalid();
+ return mTransform;
+ }
+
+ @Override
public long getTimestamp() {
throwISEIfImageIsInvalid();
return mTimestamp;
diff --git a/android/media/MediaCodecInfo.java b/android/media/MediaCodecInfo.java
index 44d90997..2a601f9b 100644
--- a/android/media/MediaCodecInfo.java
+++ b/android/media/MediaCodecInfo.java
@@ -829,14 +829,24 @@ public final class MediaCodecInfo {
/** @hide */
public CodecCapabilities dup() {
- return new CodecCapabilities(
- // clone writable arrays
- Arrays.copyOf(profileLevels, profileLevels.length),
- Arrays.copyOf(colorFormats, colorFormats.length),
- isEncoder(),
- mFlagsVerified,
- mDefaultFormat,
- mCapabilitiesInfo);
+ CodecCapabilities caps = new CodecCapabilities();
+
+ // profileLevels and colorFormats may be modified by client.
+ caps.profileLevels = Arrays.copyOf(profileLevels, profileLevels.length);
+ caps.colorFormats = Arrays.copyOf(colorFormats, colorFormats.length);
+
+ caps.mMime = mMime;
+ caps.mMaxSupportedInstances = mMaxSupportedInstances;
+ caps.mFlagsRequired = mFlagsRequired;
+ caps.mFlagsSupported = mFlagsSupported;
+ caps.mFlagsVerified = mFlagsVerified;
+ caps.mAudioCaps = mAudioCaps;
+ caps.mVideoCaps = mVideoCaps;
+ caps.mEncoderCaps = mEncoderCaps;
+ caps.mDefaultFormat = mDefaultFormat;
+ caps.mCapabilitiesInfo = mCapabilitiesInfo;
+
+ return caps;
}
/**
@@ -898,13 +908,13 @@ public final class MediaCodecInfo {
if (mMime.toLowerCase().startsWith("audio/")) {
mAudioCaps = AudioCapabilities.create(info, this);
- mAudioCaps.setDefaultFormat(mDefaultFormat);
+ mAudioCaps.getDefaultFormat(mDefaultFormat);
} else if (mMime.toLowerCase().startsWith("video/")) {
mVideoCaps = VideoCapabilities.create(info, this);
}
if (encoder) {
mEncoderCaps = EncoderCapabilities.create(info, this);
- mEncoderCaps.setDefaultFormat(mDefaultFormat);
+ mEncoderCaps.getDefaultFormat(mDefaultFormat);
}
final Map<String, Object> global = MediaCodecList.getGlobalSettings();
@@ -990,8 +1000,7 @@ public final class MediaCodecInfo {
return caps;
}
- /** @hide */
- public void init(MediaFormat info, CodecCapabilities parent) {
+ private void init(MediaFormat info, CodecCapabilities parent) {
mParent = parent;
initWithPlatformLimits();
applyLevelLimits();
@@ -1171,7 +1180,7 @@ public final class MediaCodecInfo {
}
/** @hide */
- public void setDefaultFormat(MediaFormat format) {
+ public void getDefaultFormat(MediaFormat format) {
// report settings that have only a single choice
if (mBitrateRange.getLower().equals(mBitrateRange.getUpper())) {
format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrateRange.getLower());
@@ -1585,8 +1594,7 @@ public final class MediaCodecInfo {
return caps;
}
- /** @hide */
- public void init(MediaFormat info, CodecCapabilities parent) {
+ private void init(MediaFormat info, CodecCapabilities parent) {
mParent = parent;
initWithPlatformLimits();
applyLevelLimits();
@@ -2707,8 +2715,7 @@ public final class MediaCodecInfo {
return caps;
}
- /** @hide */
- public void init(MediaFormat info, CodecCapabilities parent) {
+ private void init(MediaFormat info, CodecCapabilities parent) {
// no support for complexity or quality yet
mParent = parent;
mComplexityRange = Range.create(0, 0);
@@ -2789,7 +2796,7 @@ public final class MediaCodecInfo {
}
/** @hide */
- public void setDefaultFormat(MediaFormat format) {
+ public void getDefaultFormat(MediaFormat format) {
// don't list trivial quality/complexity as default for now
if (!mQualityRange.getUpper().equals(mQualityRange.getLower())
&& mDefaultQuality != null) {
@@ -3002,6 +3009,7 @@ public final class MediaCodecInfo {
// from OMX_VIDEO_HEVCPROFILETYPE
public static final int HEVCProfileMain = 0x01;
public static final int HEVCProfileMain10 = 0x02;
+ public static final int HEVCProfileMainStill = 0x04;
public static final int HEVCProfileMain10HDR10 = 0x1000;
// from OMX_VIDEO_HEVCLEVELTYPE
@@ -3078,6 +3086,23 @@ public final class MediaCodecInfo {
* {@link VideoCapabilities} to determine the codec capabilities.
*/
public int level;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof CodecProfileLevel) {
+ CodecProfileLevel other = (CodecProfileLevel)obj;
+ return other.profile == profile && other.level == level;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(((long)profile << Integer.SIZE) | level);
+ }
};
/**
diff --git a/android/media/MediaController2.java b/android/media/MediaController2.java
index d669bc12..591f33f5 100644
--- a/android/media/MediaController2.java
+++ b/android/media/MediaController2.java
@@ -16,18 +16,22 @@
package android.media;
+import static android.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.Context;
-import android.media.MediaSession2.Command;
+import android.media.MediaPlaylistAgent.RepeatMode;
+import android.media.MediaPlaylistAgent.ShuffleMode;
import android.media.MediaSession2.CommandButton;
-import android.media.MediaSession2.CommandGroup;
import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.PlaylistParam;
+import android.media.MediaSession2.ErrorCode;
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;
@@ -36,6 +40,7 @@ import java.util.List;
import java.util.concurrent.Executor;
/**
+ * @hide
* 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.
@@ -48,9 +53,9 @@ import java.util.concurrent.Executor;
* <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.
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)} for the service.
+ * Wait {@link ControllerCallback#onConnected(MediaController2, SessionCommandGroup2)} or
+ * {@link ControllerCallback#onDisconnected(MediaController2)} 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
@@ -61,10 +66,7 @@ import java.util.concurrent.Executor;
* <p>
* @see MediaSession2
* @see MediaSessionService2
- * @hide
*/
-// TODO(jaewan): Unhide
-// TODO(jaewan): Revisit comments. Currently MediaBrowser case is missing.
public class MediaController2 implements AutoCloseable {
/**
* Interface for listening to change in activeness of the {@link MediaSession2}. It's
@@ -75,9 +77,11 @@ public class MediaController2 implements AutoCloseable {
* Called when the controller is successfully connected to the session. The controller
* becomes available afterwards.
*
+ * @param controller the controller for this event
* @param allowedCommands commands that's allowed by the session.
*/
- public void onConnected(CommandGroup allowedCommands) { }
+ public void onConnected(@NonNull MediaController2 controller,
+ @NonNull SessionCommandGroup2 allowedCommands) { }
/**
* Called when the session refuses the controller or the controller is disconnected from
@@ -86,58 +90,159 @@ public class MediaController2 implements AutoCloseable {
* <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.
+ *
+ * @param controller the controller for this event
+ * @param controller controller for this event
*/
- public void onDisconnected() { }
+ public void onDisconnected(@NonNull MediaController2 controller) { }
/**
* 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.
+ * Can be called before {@link #onConnected(MediaController2, SessionCommandGroup2)} is
+ * called.
*
+ * @param controller the controller for this event
* @param layout
*/
- public void onCustomLayoutChanged(List<CommandButton> layout) { }
+ public void onCustomLayoutChanged(@NonNull MediaController2 controller,
+ @NonNull List<CommandButton> layout) { }
/**
* Called when the session has changed anything related with the {@link PlaybackInfo}.
*
+ * @param controller the controller for this event
* @param info new playback info
*/
- public void onAudioInfoChanged(PlaybackInfo info) { }
+ public void onPlaybackInfoChanged(@NonNull MediaController2 controller,
+ @NonNull PlaybackInfo info) { }
/**
* Called when the allowed commands are changed by session.
*
+ * @param controller the controller for this event
* @param commands newly allowed commands
*/
- public void onAllowedCommandsChanged(CommandGroup commands) { }
+ public void onAllowedCommandsChanged(@NonNull MediaController2 controller,
+ @NonNull SessionCommandGroup2 commands) { }
/**
* Called when the session sent a custom command.
*
+ * @param controller the controller for this event
* @param command
* @param args
* @param receiver
*/
- public void onCustomCommand(Command command, @Nullable Bundle args,
+ public void onCustomCommand(@NonNull MediaController2 controller,
+ @NonNull SessionCommand2 command, @Nullable Bundle args,
@Nullable ResultReceiver receiver) { }
/**
- * Called when the playlist is changed.
+ * Called when the player state is changed.
*
- * @param list
- * @param param
+ * @param controller the controller for this event
+ * @param state
*/
- public void onPlaylistChanged(
- @NonNull List<MediaItem2> list, @NonNull PlaylistParam param) { }
+ public void onPlayerStateChanged(@NonNull MediaController2 controller, int state) { }
/**
- * Called when the playback state is changed.
+ * Called when playback speed is changed.
*
- * @param state
+ * @param controller the controller for this event
+ * @param speed speed
+ */
+ public void onPlaybackSpeedChanged(@NonNull MediaController2 controller,
+ float speed) { }
+
+ /**
+ * Called to report buffering events for a data source.
+ * <p>
+ * Use {@link #getBufferedPosition()} for current buffering position.
+ *
+ * @param controller the controller for this event
+ * @param item the media item for which buffering is happening.
+ * @param state the new buffering state.
+ */
+ public void onBufferingStateChanged(@NonNull MediaController2 controller,
+ @NonNull MediaItem2 item, @MediaPlayerBase.BuffState int state) { }
+
+ /**
+ * Called to indicate that seeking is completed.
+ *
+ * @param controller the controller for this event.
+ * @param position the previous seeking request.
+ */
+ public void onSeekCompleted(@NonNull MediaController2 controller, long position) { }
+
+ /**
+ * Called when a error from
+ *
+ * @param controller the controller for this event
+ * @param errorCode error code
+ * @param extras extra information
+ */
+ public void onError(@NonNull MediaController2 controller, @ErrorCode int errorCode,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when the player's currently playing item is changed
+ * <p>
+ * When it's called, you should invalidate previous playback information and wait for later
+ * callbacks.
+ *
+ * @param controller the controller for this event
+ * @param item new item
+ * @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
+ */
+ // TODO(jaewan): Use this (b/74316764)
+ public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
+ @NonNull MediaItem2 item) { }
+
+ /**
+ * Called when a playlist is changed.
+ *
+ * @param controller the controller for this event
+ * @param list new playlist
+ * @param metadata new metadata
+ */
+ public void onPlaylistChanged(@NonNull MediaController2 controller,
+ @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when a playlist metadata is changed.
+ *
+ * @param controller the controller for this event
+ * @param metadata new metadata
+ */
+ public void onPlaylistMetadataChanged(@NonNull MediaController2 controller,
+ @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when the shuffle mode is changed.
+ *
+ * @param controller the controller for this event
+ * @param shuffleMode repeat mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public void onShuffleModeChanged(@NonNull MediaController2 controller,
+ @MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
+
+ /**
+ * Called when the repeat mode is changed.
+ *
+ * @param controller the controller for this event
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
*/
- public void onPlaybackStateChanged(@NonNull PlaybackState2 state) { }
+ public void onRepeatModeChanged(@NonNull MediaController2 controller,
+ @MediaPlaylistAgent.RepeatMode int repeatMode) { }
}
/**
@@ -155,21 +260,20 @@ public class MediaController2 implements AutoCloseable {
*/
public static final int PLAYBACK_TYPE_LOCAL = 1;
- private final int mVolumeType;
- private final int mVolumeControl;
- private final int mMaxVolume;
- private final int mCurrentVolume;
- private final AudioAttributes mAudioAttrs;
+ private final PlaybackInfoProvider mProvider;
+
+ /**
+ * @hide
+ */
+ public PlaybackInfo(PlaybackInfoProvider provider) {
+ mProvider = provider;
+ }
/**
* @hide
*/
- public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
- mVolumeType = type;
- mAudioAttrs = attrs;
- mVolumeControl = control;
- mMaxVolume = max;
- mCurrentVolume = current;
+ public PlaybackInfoProvider getProvider() {
+ return mProvider;
}
/**
@@ -182,7 +286,7 @@ public class MediaController2 implements AutoCloseable {
* @return The type of playback this session is using.
*/
public int getPlaybackType() {
- return mVolumeType;
+ return mProvider.getPlaybackType_impl();
}
/**
@@ -194,22 +298,21 @@ public class MediaController2 implements AutoCloseable {
* @return The attributes for this session.
*/
public AudioAttributes getAudioAttributes() {
- return mAudioAttrs;
+ return mProvider.getAudioAttributes_impl();
}
/**
* Get the type of volume control that can be used. One of:
* <ul>
- * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
- * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
- * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
+ * <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.
+ * @return The type of volume control that may be used with this session.
*/
- public int getVolumeControl() {
- return mVolumeControl;
+ public int getControlType() {
+ return mProvider.getControlType_impl();
}
/**
@@ -218,7 +321,7 @@ public class MediaController2 implements AutoCloseable {
* @return The maximum allowed volume where this session is playing.
*/
public int getMaxVolume() {
- return mMaxVolume;
+ return mProvider.getMaxVolume_impl();
}
/**
@@ -227,39 +330,39 @@ public class MediaController2 implements AutoCloseable {
* @return The current volume where this session is playing.
*/
public int getCurrentVolume() {
- return mCurrentVolume;
+ 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.
+ * 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 callback controller callback to receive changes in
* @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 ControllerCallback callback, @NonNull Executor executor) {
+ @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 = createProvider(context, token, callback, executor);
+ mProvider.initialize();
}
MediaController2Provider createProvider(@NonNull Context context,
- @NonNull SessionToken2 token, @NonNull ControllerCallback callback,
- @NonNull Executor executor) {
- return ApiLoader.getProvider(context)
- .createMediaController2(this, context, token, callback, executor);
+ @NonNull SessionToken2 token, @NonNull Executor executor,
+ @NonNull ControllerCallback callback) {
+ return ApiLoader.getProvider().createMediaController2(
+ context, this, token, executor, callback);
}
/**
@@ -281,8 +384,7 @@ public class MediaController2 implements AutoCloseable {
/**
* @return token
*/
- public @NonNull
- SessionToken2 getSessionToken() {
+ public @NonNull SessionToken2 getSessionToken() {
return mProvider.getSessionToken_impl();
}
@@ -305,36 +407,26 @@ public class MediaController2 implements AutoCloseable {
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.
+ * state to {@link MediaPlayerBase#PLAYER_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.
+ * Fast forwards playback. 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.
+ * Rewinds playback. If playback is already rewinding this may increase the rate.
*/
public void rewind() {
mProvider.rewind_impl();
@@ -350,20 +442,11 @@ public class MediaController2 implements AutoCloseable {
}
/**
- * 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);
- }
-
- /**
+ * Revisit this API later.
* @hide
*/
public void skipForward() {
+ // TODO(jaewan): (Post-P) Discuss this API later.
// To match with KEYCODE_MEDIA_SKIP_FORWARD
}
@@ -371,6 +454,7 @@ public class MediaController2 implements AutoCloseable {
* @hide
*/
public void skipBackward() {
+ // TODO(jaewan): (Post-P) Discuss this API later.
// To match with KEYCODE_MEDIA_SKIP_BACKWARD
}
@@ -387,12 +471,9 @@ public class MediaController2 implements AutoCloseable {
/**
* 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.
+ * @param query The search query. Should not be an empty string.
+ * @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);
@@ -405,16 +486,15 @@ public class MediaController2 implements AutoCloseable {
* @param extras Optional extras that can include extra information about the media item
* to be played.
*/
- public void playFromUri(@NonNull String uri, @Nullable Bundle extras) {
+ 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,
+ * will change its playback state to {@link MediaPlayerBase#PLAYER_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.
*
@@ -423,21 +503,20 @@ public class MediaController2 implements AutoCloseable {
* to be prepared.
*/
public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
- mProvider.prepareMediaId_impl(mediaId, extras);
+ mProvider.prepareFromMediaId_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,
+ * Request that the player prepare playback for a specific search query.
+ * 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 MediaPlayerBase#PLAYER_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.
+ * @param query The search query. Should not be an empty string.
+ * @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);
@@ -447,8 +526,8 @@ public class MediaController2 implements AutoCloseable {
* 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,
+ * session will change its playback state to {@link MediaPlayerBase#PLAYER_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.
@@ -461,7 +540,7 @@ public class MediaController2 implements AutoCloseable {
/**
* Set the volume of the output this session is playing on. The command will be ignored if it
- * does not support {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}.
+ * 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}.
@@ -483,8 +562,8 @@ public class MediaController2 implements AutoCloseable {
* 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 VolumeProvider#VOLUME_CONTROL_RELATIVE} or
- * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}.
+ * {@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}.
@@ -502,39 +581,74 @@ public class MediaController2 implements AutoCloseable {
}
/**
- * 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>
+ * Get an intent for launching UI associated with this session if one exists.
*
- * @return The supported rating type
+ * @return A {@link PendingIntent} to launch UI or null.
*/
- public int getRatingType() {
- return mProvider.getRatingType_impl();
+ public @Nullable PendingIntent getSessionActivity() {
+ return mProvider.getSessionActivity_impl();
}
/**
- * Get an intent for launching UI associated with this session if one exists.
+ * Get the lastly cached player state from
+ * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
*
- * @return A {@link PendingIntent} to launch UI or null.
+ * @return player state
*/
- public @Nullable PendingIntent getSessionActivity() {
- return mProvider.getSessionActivity_impl();
+ public int getPlayerState() {
+ return mProvider.getPlayerState_impl();
+ }
+
+ /**
+ * Gets the current playback position.
+ * <p>
+ * This returns the calculated value of the position, based on the difference between the
+ * update time and current time.
+ *
+ * @return position
+ */
+ public long getCurrentPosition() {
+ return mProvider.getCurrentPosition_impl();
+ }
+
+ /**
+ * Get the lastly cached playback speed from
+ * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
+ *
+ * @return speed
+ */
+ public float getPlaybackSpeed() {
+ return mProvider.getPlaybackSpeed_impl();
+ }
+
+ /**
+ * Set the playback speed.
+ */
+ public void setPlaybackSpeed(float speed) {
+ // TODO(jaewan): implement this (b/74093080)
+ }
+
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ * @return the buffering state.
+ */
+ public @MediaPlayerBase.BuffState int getBufferingState() {
+ // TODO(jaewan): Implement.
+ return BUFFERING_STATE_UNKNOWN;
}
/**
- * Get the latest {@link PlaybackState2} from the session.
+ * Gets the lastly cached buffered position from the session when
+ * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
+ * called.
*
- * @return a playback state
+ * @return buffering position in millis
*/
- public PlaybackState2 getPlaybackState() {
- return mProvider.getPlaybackState_impl();
+ public long getBufferedPosition() {
+ return mProvider.getBufferedPosition_impl();
}
/**
@@ -547,14 +661,19 @@ public class MediaController2 implements AutoCloseable {
}
/**
- * 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()}.
+ * Rate the media. This will cause the rating to be set for the current user.
+ * The rating style must follow the user rating style from the session.
+ * You can get the rating style from the session through the
+ * {@link MediaMetadata#getRating(String)} with the key
+ * {@link MediaMetadata#METADATA_KEY_USER_RATING}.
+ * <p>
+ * If the user rating was {@code null}, the media item does not accept setting user rating.
*
- * @param rating The rating to set for the current content
+ * @param mediaId The id of the media
+ * @param rating The rating to set
*/
- public void setRating(Rating2 rating) {
- mProvider.setRating_impl(rating);
+ public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
+ mProvider.setRating_impl(mediaId, rating);
}
/**
@@ -564,53 +683,189 @@ public class MediaController2 implements AutoCloseable {
* @param args optional argument
* @param cb optional result receiver
*/
- public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args,
+ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
@Nullable ResultReceiver cb) {
mProvider.sendCustomCommand_impl(command, args, cb);
}
/**
- * Return playlist from the session.
+ * Returns the cached playlist from
+ * {@link ControllerCallback#onPlaylistChanged(MediaController2, List, MediaMetadata2)}.
+ * <p>
+ * This list may differ with the list that was specified with
+ * {@link #setPlaylist(List, MediaMetadata2)} depending on the session implementation. Use media
+ * items returned here for other playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
*
- * @return playlist. Can be {@code null} if the controller doesn't have enough permission.
+ * @return The playlist. Can be {@code null} if the controller doesn't have enough permission or
+ * the session hasn't set any playlist.
*/
public @Nullable List<MediaItem2> getPlaylist() {
return mProvider.getPlaylist_impl();
}
- public @Nullable PlaylistParam getPlaylistParam() {
- return mProvider.getPlaylistParam_impl();
+ /**
+ * Sets the playlist.
+ * <p>
+ * Even when the playlist is successfully set, use the playlist returned from
+ * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
+ * Otherwise the session in the remote process can't distinguish between media items.
+ *
+ * @param list playlist
+ * @param metadata metadata of the playlist
+ * @see #getPlaylist()
+ * @see ControllerCallback#onPlaylistChanged(MediaController2, List, MediaMetadata2)
+ */
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ mProvider.setPlaylist_impl(list, metadata);
+ }
+
+ /**
+ * Updates the playlist metadata
+ *
+ * @param metadata metadata of the playlist
+ */
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ mProvider.updatePlaylistMetadata_impl(metadata);
+ }
+
+ /**
+ * Gets the lastly cached playlist playlist metadata either from
+ * {@link ControllerCallback#onPlaylistMetadataChanged(MediaController2, MediaMetadata2)} or
+ * {@link ControllerCallback#onPlaylistChanged(MediaController2, List, MediaMetadata2)}.
+ *
+ * @return metadata metadata of the playlist, or null if none is set
+ */
+ public @Nullable MediaMetadata2 getPlaylistMetadata() {
+ return mProvider.getPlaylistMetadata_impl();
}
+
/**
- * Removes the media item at index in the play list.
+ * Adds the media item to the playlist at position index. Index equals or greater than
+ * the current playlist size will add the item at the end of the playlist.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the playlist,
+ * the current index of the playlist will be incremented correspondingly.
+ *
+ * @param index the index you want to add
+ * @param item the media item you want to add
+ */
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ mProvider.addPlaylistItem_impl(index, item);
+ }
+
+ /**
+ * Removes the media item at index in the playlist.
*<p>
- * If index is same as the current index of the playlist, current playback
+ * If the item is the currently playing item 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
+ * @param item the media item you want to add
*/
- // 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) {
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
mProvider.removePlaylistItem_impl(item);
}
/**
- * Inserts the media item to the play list at position index.
+ * Replace the media item at index in the playlist. This can be also used to update metadata of
+ * an item.
+ *
+ * @param index the index of the item to replace
+ * @param item the new item
+ */
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ mProvider.replacePlaylistItem_impl(index, item);
+ }
+
+ /**
+ * Get the lastly cached current item from
+ * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
+ *
+ * @return index of the current item
+ */
+ public MediaItem2 getCurrentMediaItem() {
+ return mProvider.getCurrentMediaItem_impl();
+ }
+
+ /**
+ * Skips to the previous item in the playlist.
* <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.
+ * This calls {@link MediaSession2#skipToPreviousItem()} if the session allows.
+ */
+ public void skipToPreviousItem() {
+ mProvider.skipToPreviousItem_impl();
+ }
+
+ /**
+ * Skips to the next item in the playlist.
+ * <p>
+ * This calls {@link MediaSession2#skipToNextItem()} if the session allows.
+ */
+ public void skipToNextItem() {
+ mProvider.skipToNextItem_impl();
+ }
+
+ /**
+ * Skips to the item in the playlist.
+ * <p>
+ * This calls {@link MediaSession2#skipToPlaylistItem(MediaItem2)} if the session allows.
*
- * @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
+ * @param item The item in the playlist you want to play
*/
- public void addPlaylistItem(int index, MediaItem2 item) {
- mProvider.addPlaylistItem_impl(index, item);
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ mProvider.skipToPlaylistItem_impl(item);
+ }
+
+ /**
+ * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged(
+ * MediaController2, int)}.
+ *
+ * @return repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public @RepeatMode int getRepeatMode() {
+ return mProvider.getRepeatMode_impl();
+ }
+
+ /**
+ * Sets the repeat mode.
+ *
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ mProvider.setRepeatMode_impl(repeatMode);
+ }
+
+ /**
+ * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged(
+ * MediaController2, int)}.
+ *
+ * @return The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public @ShuffleMode int getShuffleMode() {
+ return mProvider.getShuffleMode_impl();
+ }
+
+ /**
+ * Sets the shuffle mode.
+ *
+ * @param shuffleMode The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public void setShuffleMode(@ShuffleMode int shuffleMode) {
+ mProvider.setShuffleMode_impl(shuffleMode);
}
}
diff --git a/android/media/MediaController2Test.java b/android/media/MediaController2Test.java
deleted file mode 100644
index ae67a952..00000000
--- a/android/media/MediaController2Test.java
+++ /dev/null
@@ -1,487 +0,0 @@
-/*
- * 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.media.MediaPlayerBase.PlaybackListener;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
-import android.media.TestUtils.SyncHandler;
-import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.support.test.filters.FlakyTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static android.media.TestUtils.createPlaybackState;
-import static org.junit.Assert.*;
-
-/**
- * Tests {@link MediaController2}.
- */
-// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
-// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-@FlakyTest
-public class MediaController2Test extends MediaSession2TestBase {
- private static final String TAG = "MediaController2Test";
-
- MediaSession2 mSession;
- MediaController2 mController;
- MockPlayer mPlayer;
-
- @Before
- @Override
- public void setUp() throws Exception {
- super.setUp();
- // Create this test specific MediaSession2 to use our own Handler.
- sHandler.postAndSync(()->{
- mPlayer = new MockPlayer(1);
- mSession = new MediaSession2.Builder(mContext, mPlayer).setId(TAG).build();
- });
-
- mController = createController(mSession.getToken());
- TestServiceRegistry.getInstance().setHandler(sHandler);
- }
-
- @After
- @Override
- public void cleanUp() throws Exception {
- super.cleanUp();
- sHandler.postAndSync(() -> {
- if (mSession != null) {
- mSession.close();
- }
- });
- TestServiceRegistry.getInstance().cleanUp();
- }
-
- @Test
- public void testPlay() throws InterruptedException {
- mController.play();
- try {
- assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } catch (InterruptedException e) {
- fail(e.getMessage());
- }
- assertTrue(mPlayer.mPlayCalled);
- }
-
- @Test
- public void testPause() throws InterruptedException {
- mController.pause();
- try {
- assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } catch (InterruptedException e) {
- fail(e.getMessage());
- }
- assertTrue(mPlayer.mPauseCalled);
- }
-
-
- @Test
- public void testSkipToPrevious() throws InterruptedException {
- mController.skipToPrevious();
- try {
- assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } catch (InterruptedException e) {
- fail(e.getMessage());
- }
- assertTrue(mPlayer.mSkipToPreviousCalled);
- }
-
- @Test
- public void testSkipToNext() throws InterruptedException {
- mController.skipToNext();
- try {
- assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } catch (InterruptedException e) {
- fail(e.getMessage());
- }
- assertTrue(mPlayer.mSkipToNextCalled);
- }
-
- @Test
- public void testStop() throws InterruptedException {
- mController.stop();
- try {
- assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } catch (InterruptedException e) {
- fail(e.getMessage());
- }
- assertTrue(mPlayer.mStopCalled);
- }
-
- @Test
- public void testGetPackageName() {
- assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
- }
-
- @Test
- public void testGetPlaybackState() throws InterruptedException {
- // TODO(jaewan): add equivalent test later
- /*
- final CountDownLatch latch = new CountDownLatch(1);
- final MediaPlayerBase.PlaybackListener listener = (state) -> {
- assertEquals(PlaybackState.STATE_BUFFERING, state.getState());
- latch.countDown();
- };
- assertNull(mController.getPlaybackState());
- mController.addPlaybackListener(listener, sHandler);
-
- mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_BUFFERING));
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- assertEquals(PlaybackState.STATE_BUFFERING, mController.getPlaybackState().getState());
- */
- }
-
- // TODO(jaewan): add equivalent test later
- /*
- @Test
- public void testAddPlaybackListener() throws InterruptedException {
- final CountDownLatch latch = new CountDownLatch(2);
- final MediaPlayerBase.PlaybackListener listener = (state) -> {
- switch ((int) latch.getCount()) {
- case 2:
- assertEquals(PlaybackState.STATE_PLAYING, state.getState());
- break;
- case 1:
- assertEquals(PlaybackState.STATE_PAUSED, state.getState());
- break;
- }
- latch.countDown();
- };
-
- mController.addPlaybackListener(listener, sHandler);
- sHandler.postAndSync(()->{
- mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
- mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
- });
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- }
-
- @Test
- public void testRemovePlaybackListener() throws InterruptedException {
- final CountDownLatch latch = new CountDownLatch(1);
- final MediaPlayerBase.PlaybackListener listener = (state) -> {
- fail();
- latch.countDown();
- };
- mController.addPlaybackListener(listener, sHandler);
- mController.removePlaybackListener(listener);
- mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
- assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
- */
-
- @Test
- public void testControllerCallback_onConnected() throws InterruptedException {
- // createController() uses controller callback to wait until the controller becomes
- // available.
- MediaController2 controller = createController(mSession.getToken());
- assertNotNull(controller);
- }
-
- @Test
- public void testControllerCallback_sessionRejects() throws InterruptedException {
- final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
- @Override
- public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
- return null;
- }
- };
- sHandler.postAndSync(() -> {
- mSession.close();
- mSession = new MediaSession2.Builder(mContext, mPlayer)
- .setSessionCallback(sHandlerExecutor, sessionCallback).build();
- });
- MediaController2 controller =
- createController(mSession.getToken(), false, null);
- assertNotNull(controller);
- waitForConnect(controller, false);
- waitForDisconnect(controller, true);
- }
-
- @Test
- public void testControllerCallback_releaseSession() throws InterruptedException {
- sHandler.postAndSync(() -> {
- mSession.close();
- });
- waitForDisconnect(mController, true);
- }
-
- @Test
- public void testControllerCallback_release() throws InterruptedException {
- mController.close();
- waitForDisconnect(mController, true);
- }
-
- @Test
- public void testIsConnected() throws InterruptedException {
- assertTrue(mController.isConnected());
- sHandler.postAndSync(()->{
- mSession.close();
- });
- // postAndSync() to wait until the disconnection is propagated.
- sHandler.postAndSync(()->{
- assertFalse(mController.isConnected());
- });
- }
-
- /**
- * Test potential deadlock for calls between controller and session.
- */
- @Test
- public void testDeadlock() throws InterruptedException {
- sHandler.postAndSync(() -> {
- mSession.close();
- mSession = null;
- });
-
- // Two more threads are needed not to block test thread nor test wide thread (sHandler).
- final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
- final HandlerThread testThread = new HandlerThread("testDeadlock_test");
- sessionThread.start();
- testThread.start();
- final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
- final Handler testHandler = new Handler(testThread.getLooper());
- final CountDownLatch latch = new CountDownLatch(1);
- try {
- final MockPlayer player = new MockPlayer(0);
- sessionHandler.postAndSync(() -> {
- mSession = new MediaSession2.Builder(mContext, mPlayer)
- .setId("testDeadlock").build();
- });
- final MediaController2 controller = createController(mSession.getToken());
- testHandler.post(() -> {
- final PlaybackState2 state = createPlaybackState(PlaybackState.STATE_ERROR);
- for (int i = 0; i < 100; i++) {
- // triggers call from session to controller.
- player.notifyPlaybackState(state);
- // triggers call from controller to session.
- controller.play();
-
- // Repeat above
- player.notifyPlaybackState(state);
- controller.pause();
- player.notifyPlaybackState(state);
- controller.stop();
- player.notifyPlaybackState(state);
- controller.skipToNext();
- player.notifyPlaybackState(state);
- controller.skipToPrevious();
- }
- // This may hang if deadlock happens.
- latch.countDown();
- });
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } finally {
- if (mSession != null) {
- sessionHandler.postAndSync(() -> {
- // Clean up here because sessionHandler will be removed afterwards.
- mSession.close();
- mSession = null;
- });
- }
- if (sessionThread != null) {
- sessionThread.quitSafely();
- }
- if (testThread != null) {
- testThread.quitSafely();
- }
- }
- }
-
- @Ignore
- @Test
- public void testGetServiceToken() {
- SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
- assertNotNull(token);
- assertEquals(mContext.getPackageName(), token.getPackageName());
- assertEquals(MockMediaSessionService2.ID, token.getId());
- assertNull(token.getSessionBinder());
- assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
- }
-
- private void connectToService(SessionToken2 token) throws InterruptedException {
- mController = createController(token);
- mSession = TestServiceRegistry.getInstance().getServiceInstance().getSession();
- mPlayer = (MockPlayer) mSession.getPlayer();
- }
-
- // TODO(jaewan): Reenable when session manager detects app installs
- @Ignore
- @Test
- public void testConnectToService_sessionService() throws InterruptedException {
- connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
- testConnectToService();
- }
-
- // TODO(jaewan): Reenable when session manager detects app installs
- @Ignore
- @Test
- public void testConnectToService_libraryService() throws InterruptedException {
- connectToService(TestUtils.getServiceToken(mContext, MockMediaLibraryService2.ID));
- testConnectToService();
- }
-
- public void testConnectToService() throws InterruptedException {
- TestServiceRegistry serviceInfo = TestServiceRegistry.getInstance();
- ControllerInfo info = serviceInfo.getOnConnectControllerInfo();
- assertEquals(mContext.getPackageName(), info.getPackageName());
- assertEquals(Process.myUid(), info.getUid());
- assertFalse(info.isTrusted());
-
- // Test command from controller to session service
- mController.play();
- assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- assertTrue(mPlayer.mPlayCalled);
-
- // Test command from session service to controller
- // TODO(jaewan): Add equivalent tests again
- /*
- final CountDownLatch latch = new CountDownLatch(1);
- mController.addPlaybackListener((state) -> {
- assertNotNull(state);
- assertEquals(PlaybackState.STATE_REWINDING, state.getState());
- latch.countDown();
- }, sHandler);
- mPlayer.notifyPlaybackState(
- TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- */
- }
-
- @Test
- public void testControllerAfterSessionIsGone_session() throws InterruptedException {
- testControllerAfterSessionIsGone(mSession.getToken().getId());
- }
-
- @Ignore
- @Test
- public void testControllerAfterSessionIsGone_sessionService() throws InterruptedException {
- connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
- testControllerAfterSessionIsGone(MockMediaSessionService2.ID);
- }
-
- @Test
- public void testClose_beforeConnected() throws InterruptedException {
- MediaController2 controller =
- createController(mSession.getToken(), false, null);
- controller.close();
- }
-
- @Test
- public void testClose_twice() throws InterruptedException {
- mController.close();
- mController.close();
- }
-
- @Test
- public void testClose_session() throws InterruptedException {
- final String id = mSession.getToken().getId();
- mController.close();
- // close is done immediately for session.
- testNoInteraction();
-
- // Test whether the controller is notified about later close of the session or
- // re-creation.
- testControllerAfterSessionIsGone(id);
- }
-
- // TODO(jaewan): Reenable when session manager detects app installs
- @Ignore
- @Test
- public void testClose_sessionService() throws InterruptedException {
- connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
- testCloseFromService();
- }
-
- // TODO(jaewan): Reenable when session manager detects app installs
- @Ignore
- @Test
- public void testClose_libraryService() throws InterruptedException {
- connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
- testCloseFromService();
- }
-
- private void testCloseFromService() throws InterruptedException {
- final String id = mController.getSessionToken().getId();
- final CountDownLatch latch = new CountDownLatch(1);
- TestServiceRegistry.getInstance().setServiceInstanceChangedCallback((service) -> {
- if (service == null) {
- // Destroying..
- latch.countDown();
- }
- });
- mController.close();
- // Wait until close triggers onDestroy() of the session service.
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- assertNull(TestServiceRegistry.getInstance().getServiceInstance());
- testNoInteraction();
-
- // Test whether the controller is notified about later close of the session or
- // re-creation.
- testControllerAfterSessionIsGone(id);
- }
-
- private void testControllerAfterSessionIsGone(final String id) throws InterruptedException {
- sHandler.postAndSync(() -> {
- // TODO(jaewan): Use Session.close later when we add the API.
- mSession.close();
- });
- waitForDisconnect(mController, true);
- testNoInteraction();
-
- // Test with the newly created session.
- sHandler.postAndSync(() -> {
- // Recreated session has different session stub, so previously created controller
- // shouldn't be available.
- mSession = new MediaSession2.Builder(mContext, mPlayer).setId(id).build();
- });
- testNoInteraction();
- }
-
- private void testNoInteraction() throws InterruptedException {
- final CountDownLatch latch = new CountDownLatch(1);
- final PlaybackListener playbackListener = (state) -> {
- fail("Controller shouldn't be notified about change in session after the close.");
- latch.countDown();
- };
- // TODO(jaewan): Add equivalent tests again
- /*
- mController.addPlaybackListener(playbackListener, sHandler);
- mPlayer.notifyPlaybackState(TestUtils.createPlaybackState(PlaybackState.STATE_BUFFERING));
- assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- mController.removePlaybackListener(playbackListener);
- */
- }
-
- // TODO(jaewan): Add test for service connect rejection, when we differentiate session
- // active/inactive and connection accept/refuse
-}
diff --git a/android/media/MediaDataSource.java b/android/media/MediaDataSource.java
index 948da0b9..4ba2120f 100644
--- a/android/media/MediaDataSource.java
+++ b/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/android/media/MediaDescrambler.java b/android/media/MediaDescrambler.java
index 40c837b1..99bd2549 100644
--- a/android/media/MediaDescrambler.java
+++ b/android/media/MediaDescrambler.java
@@ -125,6 +125,38 @@ public final class MediaDescrambler implements AutoCloseable {
}
/**
+ * Scramble control value indicating that the samples are not scrambled.
+ * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+ */
+ public static final byte SCRAMBLE_CONTROL_UNSCRAMBLED = 0;
+
+ /**
+ * Scramble control value reserved and shouldn't be used currently.
+ * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+ */
+ public static final byte SCRAMBLE_CONTROL_RESERVED = 1;
+
+ /**
+ * Scramble control value indicating that the even key is used.
+ * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+ */
+ public static final byte SCRAMBLE_CONTROL_EVEN_KEY = 2;
+
+ /**
+ * Scramble control value indicating that the odd key is used.
+ * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+ */
+ public static final byte SCRAMBLE_CONTROL_ODD_KEY = 3;
+
+ /**
+ * Scramble flag for a hint indicating that the descrambling request is for
+ * retrieving the PES header info only.
+ *
+ * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+ */
+ public static final byte SCRAMBLE_FLAG_PES_HEADER = (1 << 0);
+
+ /**
* Descramble a ByteBuffer of data described by a
* {@link android.media.MediaCodec.CryptoInfo} structure.
*
@@ -133,7 +165,15 @@ public final class MediaDescrambler implements AutoCloseable {
* @param dstBuf ByteBuffer to hold the descrambled data, which starts at
* dstBuf.position().
* @param cryptoInfo a {@link android.media.MediaCodec.CryptoInfo} structure
- * describing the subsamples contained in src.
+ * describing the subsamples contained in srcBuf. The iv and mode fields in
+ * CryptoInfo are not used. key[0] contains the MPEG2TS scrambling control bits
+ * (as defined in ETSI TS 100 289 (2011): "Digital Video Broadcasting (DVB);
+ * Support for use of the DVB Scrambling Algorithm version 3 within digital
+ * broadcasting systems"), and the value must be one of {@link #SCRAMBLE_CONTROL_UNSCRAMBLED},
+ * {@link #SCRAMBLE_CONTROL_RESERVED}, {@link #SCRAMBLE_CONTROL_EVEN_KEY} or
+ * {@link #SCRAMBLE_CONTROL_ODD_KEY}. key[1] is a set of bit flags, with the
+ * only possible bit being {@link #SCRAMBLE_FLAG_PES_HEADER} currently.
+ * key[2~15] are not used.
*
* @return number of bytes that have been successfully descrambled, with negative
* values indicating errors.
@@ -169,6 +209,7 @@ public final class MediaDescrambler implements AutoCloseable {
try {
return native_descramble(
cryptoInfo.key[0],
+ cryptoInfo.key[1],
cryptoInfo.numSubSamples,
cryptoInfo.numBytesOfClearData,
cryptoInfo.numBytesOfEncryptedData,
@@ -204,7 +245,8 @@ public final class MediaDescrambler implements AutoCloseable {
private native final void native_setup(@NonNull IHwBinder decramblerBinder);
private native final void native_release();
private native final int native_descramble(
- byte key, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
+ byte key, byte flags, int numSubSamples,
+ int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
@NonNull ByteBuffer srcBuf, int srcOffset, int srcLimit,
ByteBuffer dstBuf, int dstOffset, int dstLimit) throws RemoteException;
diff --git a/android/media/MediaDrm.java b/android/media/MediaDrm.java
index 063186d7..7ac15290 100644
--- a/android/media/MediaDrm.java
+++ b/android/media/MediaDrm.java
@@ -628,14 +628,48 @@ public final class MediaDrm implements AutoCloseable {
}
/**
- * Open a new session with the MediaDrm object. A session ID is returned.
+ * Open a new session with the MediaDrm object. A session ID is returned.
+ * By default, sessions are opened at the native security level of the device.
*
* @throws NotProvisionedException if provisioning is needed
* @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 #SECURITY_LEVEL_SW_SECURE_CRYPTO},
+ * {@link #SECURITY_LEVEL_SW_SECURE_DECODE},
+ * {@link #SECURITY_LEVEL_HW_SECURE_CRYPTO},
+ * {@link #SECURITY_LEVEL_HW_SECURE_DECODE} or
+ * {@link #SECURITY_LEVEL_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
@@ -671,7 +705,9 @@ public final class MediaDrm implements AutoCloseable {
public @interface KeyType {}
/**
- * Contains the opaque data an app uses to request keys from a license server
+ * Contains the opaque data an app uses to request keys from a license server.
+ * These request types may or may not be generated by a given plugin. Refer
+ * to plugin vendor documentation for more information.
*/
public static final class KeyRequest {
private byte[] mData;
@@ -696,8 +732,8 @@ public final class MediaDrm implements AutoCloseable {
public static final int REQUEST_TYPE_RELEASE = 2;
/**
- * Keys are already loaded. No license request is necessary, and no
- * key request data is returned.
+ * Keys are already loaded and are available for use. No license request is necessary, and
+ * no key request data is returned.
*/
public static final int REQUEST_TYPE_NONE = 3;
@@ -942,43 +978,84 @@ public final class MediaDrm implements AutoCloseable {
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>
+ * 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>
- * Information from the server related to the current playback session is written
- * to persistent storage on the device when each MediaCrypto object is created.
+ * 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}.
* <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().
+ * Each secure stop has a unique ID that can be used to identify it during
+ * enumeration, access and removal.
+ * @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.
+ * The secure stop ID can be used to access or remove the corresponding
+ * secure stop.
*
- * @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.
+ * Each secure stop has a unique 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 releaseAllSecureStops();
+ 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,
@@ -1073,8 +1150,9 @@ public final class MediaDrm implements AutoCloseable {
* implementation.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({SECURITY_LEVEL_UNKNOWN, SW_SECURE_CRYPTO, SW_SECURE_DECODE,
- HW_SECURE_CRYPTO, HW_SECURE_DECODE, HW_SECURE_ALL})
+ @IntDef({SECURITY_LEVEL_UNKNOWN, SECURITY_LEVEL_SW_SECURE_CRYPTO,
+ SECURITY_LEVEL_SW_SECURE_DECODE, SECURITY_LEVEL_HW_SECURE_CRYPTO,
+ SECURITY_LEVEL_HW_SECURE_DECODE, SECURITY_LEVEL_HW_SECURE_ALL})
public @interface SecurityLevel {}
/**
@@ -1084,65 +1162,66 @@ public final class MediaDrm implements AutoCloseable {
public static final int SECURITY_LEVEL_UNKNOWN = 0;
/**
- * Software-based whitebox crypto
+ * DRM key management uses software-based whitebox crypto.
*/
- public static final int SW_SECURE_CRYPTO = 1;
+ public static final int SECURITY_LEVEL_SW_SECURE_CRYPTO = 1;
/**
- * Software-based whitebox crypto and an obfuscated decoder
+ * DRM key management and decoding use software-based whitebox crypto.
*/
- public static final int SW_SECURE_DECODE = 2;
+ public static final int SECURITY_LEVEL_SW_SECURE_DECODE = 2;
/**
- * DRM key management and crypto operations are performed within a
- * hardware backed trusted execution environment
+ * DRM key management and crypto operations are performed within a hardware
+ * backed trusted execution environment.
*/
- public static final int HW_SECURE_CRYPTO = 3;
+ public static final int SECURITY_LEVEL_HW_SECURE_CRYPTO = 3;
/**
- * DRM key management, crypto operations and decoding of content
- * are performed within a hardware backed trusted execution environment
+ * 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;
+ public static final int SECURITY_LEVEL_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.
+ * handling of the media (compressed and uncompressed) is handled within a
+ * hardware backed trusted execution environment.
+ */
+ public static final int SECURITY_LEVEL_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.
*/
- public static final int HW_SECURE_ALL = 5;
+ @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 adjusted using {@link #setSecurityLevel}.
+ * 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}.
+ * {@link #SECURITY_LEVEL_SW_SECURE_CRYPTO}, {@link #SECURITY_LEVEL_SW_SECURE_DECODE},
+ * {@link #SECURITY_LEVEL_HW_SECURE_CRYPTO}, {@link #SECURITY_LEVEL_HW_SECURE_DECODE} or
+ * {@link #SECURITY_LEVEL_HW_SECURE_ALL}.
*/
@SecurityLevel
public native int getSecurityLevel(@NonNull byte[] sessionId);
/**
- * Set the security level of a session. This can be useful if specific
- * attributes of a lower security level are needed by an application,
- * such as image manipulation or compositing. Reducing the security
- * level will typically limit decryption to lower content resolutions,
- * depending on the license policy.
- * @param sessionId the session to set the security level on.
- * @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}.
- */
- public native void setSecurityLevel(@NonNull byte[] sessionId,
- @SecurityLevel int level);
-
- /**
* String property name: identifies the maker of the DRM plugin
*/
public static final String PROPERTY_VENDOR = "vendor";
@@ -1253,8 +1332,6 @@ public final class MediaDrm implements AutoCloseable {
*
* Additional vendor-specific fields may also be present in
* the return value.
- *
- * @hide - not part of the public API at this time
*/
public PersistableBundle getMetrics() {
PersistableBundle bundle = getMetricsNative();
@@ -1571,8 +1648,6 @@ public final class MediaDrm implements AutoCloseable {
/**
* Definitions for the metrics that are reported via the
* {@link #getMetrics} call.
- *
- * @hide - not part of the public API at this time
*/
public final static class MetricsConstants
{
@@ -1582,16 +1657,350 @@ public final class MediaDrm implements AutoCloseable {
* 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";
+ = "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";
+ = "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/android/media/MediaExtractor.java b/android/media/MediaExtractor.java
index 2c1b4b35..4919eeb4 100644
--- a/android/media/MediaExtractor.java
+++ b/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/android/media/MediaFormat.java b/android/media/MediaFormat.java
index e9128e4c..384326f1 100644
--- a/android/media/MediaFormat.java
+++ b/android/media/MediaFormat.java
@@ -87,6 +87,7 @@ import java.util.Map;
* <tr><td>{@link #KEY_AAC_DRC_ATTENUATION_FACTOR}</td><td>Integer</td><td><b>decoder-only</b>, optional, if content is AAC audio, specifies the DRC attenuation factor.</td></tr>
* <tr><td>{@link #KEY_AAC_DRC_HEAVY_COMPRESSION}</td><td>Integer</td><td><b>decoder-only</b>, optional, if content is AAC audio, specifies whether to use heavy compression.</td></tr>
* <tr><td>{@link #KEY_AAC_MAX_OUTPUT_CHANNEL_COUNT}</td><td>Integer</td><td><b>decoder-only</b>, optional, if content is AAC audio, specifies the maximum number of channels the decoder outputs.</td></tr>
+ * <tr><td>{@link #KEY_AAC_DRC_EFFECT_TYPE}</td><td>Integer</td><td><b>decoder-only</b>, optional, if content is AAC audio, specifies the DRC effect type to use.</td></tr>
* <tr><td>{@link #KEY_CHANNEL_MASK}</td><td>Integer</td><td>optional, a mask of audio channel assignments</td></tr>
* <tr><td>{@link #KEY_FLAC_COMPRESSION_LEVEL}</td><td>Integer</td><td><b>encoder-only</b>, optional, if content is FLAC audio, specifies the desired compression level.</td></tr>
* </table>
@@ -104,10 +105,10 @@ import java.util.Map;
* <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_TILE_WIDTH}</td><td>Integer</td><td>required if the image has grid</td></tr>
+ * <tr><td>{@link #KEY_TILE_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>
+ * <tr><td>{@link #KEY_GRID_COLUMNS}</td><td>Integer</td><td>required if the image has grid</td></tr>
* </table>
*/
public final class MediaFormat {
@@ -149,17 +150,17 @@ public final class MediaFormat {
* 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
+ * {@link #KEY_TILE_WIDTH}, {@link #KEY_TILE_HEIGHT},
+ * {@link #KEY_GRID_ROWS}, {@link #KEY_GRID_COLUMNS} 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
+ * {@link #KEY_TILE_WIDTH} and {@link #KEY_TILE_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,
+ * by {@link #KEY_GRID_COLUMNS} 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
@@ -173,10 +174,20 @@ public final class MediaFormat {
public static final String MIMETYPE_TEXT_VTT = "text/vtt";
/**
+ * MIME type for SubRip (SRT) container.
+ */
+ public static final String MIMETYPE_TEXT_SUBRIP = "application/x-subrip";
+
+ /**
* MIME type for CEA-608 closed caption data.
*/
public static final String MIMETYPE_TEXT_CEA_608 = "text/cea-608";
+ /**
+ * MIME type for CEA-708 closed caption data.
+ */
+ public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708";
+
private Map<String, Object> mMap;
/**
@@ -274,28 +285,28 @@ 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.
+ * A key describing the width (in pixels) of each tile 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_TILE_HEIGHT
* @see #KEY_GRID_ROWS
- * @see #KEY_GRID_COLS
+ * @see #KEY_GRID_COLUMNS
*/
- public static final String KEY_GRID_WIDTH = "grid-width";
+ public static final String KEY_TILE_WIDTH = "tile-width";
/**
- * A key describing the grid height of the content in a {@link #MIMETYPE_IMAGE_ANDROID_HEIC}
- * track. The associated value is an integer.
+ * A key describing the height (in pixels) of each tile 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_TILE_WIDTH
* @see #KEY_GRID_ROWS
- * @see #KEY_GRID_COLS
+ * @see #KEY_GRID_COLUMNS
*/
- public static final String KEY_GRID_HEIGHT = "grid-height";
+ public static final String KEY_TILE_HEIGHT = "tile-height";
/**
* A key describing the number of grid rows in the content in a
@@ -303,9 +314,9 @@ public final class MediaFormat {
*
* 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
+ * @see #KEY_TILE_WIDTH
+ * @see #KEY_TILE_HEIGHT
+ * @see #KEY_GRID_COLUMNS
*/
public static final String KEY_GRID_ROWS = "grid-rows";
@@ -315,11 +326,11 @@ public final class MediaFormat {
*
* Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks.
*
- * @see #KEY_GRID_WIDTH
- * @see #KEY_GRID_HEIGHT
+ * @see #KEY_TILE_WIDTH
+ * @see #KEY_TILE_HEIGHT
* @see #KEY_GRID_ROWS
*/
- public static final String KEY_GRID_COLS = "grid-cols";
+ public static final String KEY_GRID_COLUMNS = "grid-cols";
/**
* A key describing the raw audio sample encoding/format.
@@ -512,6 +523,31 @@ public final class MediaFormat {
public static final String KEY_AAC_DRC_TARGET_REFERENCE_LEVEL = "aac-target-ref-level";
/**
+ * A key describing for selecting the DRC effect type for MPEG-D DRC.
+ * The supported values are defined in ISO/IEC 23003-4:2015 and are described as follows:
+ * <table>
+ * <tr><th>Value</th><th>Effect</th></tr>
+ * <tr><th>-1</th><th>Off</th></tr>
+ * <tr><th>0</th><th>None</th></tr>
+ * <tr><th>1</th><th>Late night</th></tr>
+ * <tr><th>2</th><th>Noisy environment</th></tr>
+ * <tr><th>3</th><th>Limited playback range</th></tr>
+ * <tr><th>4</th><th>Low playback level</th></tr>
+ * <tr><th>5</th><th>Dialog enhancement</th></tr>
+ * <tr><th>6</th><th>General compression</th></tr>
+ * </table>
+ * <p>The value -1 (Off) disables DRC processing, while loudness normalization may still be
+ * active and dependent on KEY_AAC_DRC_TARGET_REFERENCE_LEVEL.<br>
+ * The value 0 (None) automatically enables DRC processing if necessary to prevent signal
+ * clipping<br>
+ * The value 6 (General compression) can be used for enabling MPEG-D DRC without particular
+ * DRC effect type request.<br>
+ * The default is DRC effect type "None".
+ * <p>This key is only used during decoding.
+ */
+ public static final String KEY_AAC_DRC_EFFECT_TYPE = "aac-drc-effect-type";
+
+ /**
* A key describing the target reference level that was assumed at the encoder for
* calculation of attenuation gains for clipping prevention. This information can be provided
* if it is known, otherwise a worst-case assumption is used.
diff --git a/android/media/MediaItem2.java b/android/media/MediaItem2.java
index 96a87d5d..423a1fd4 100644
--- a/android/media/MediaItem2.java
+++ b/android/media/MediaItem2.java
@@ -19,31 +19,23 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.media.update.ApiLoader;
+import android.media.update.MediaItem2Provider;
import android.os.Bundle;
-import android.text.TextUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
+ * @hide
* 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
*/
-// TODO(jaewan): Unhide and extends from DataSourceDesc.
-// Note) Feels like an anti-pattern. We should anonymize MediaItem2 to remove *all*
-// information in the DataSourceDesc. Why it should extends from this?
-// TODO(jaewan): Move this to updatable
-// Previously MediaBrowser.MediaItem
public class MediaItem2 {
- private final int mFlags;
- private MediaMetadata2 mMetadata;
-
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
@@ -62,15 +54,21 @@ public class MediaItem2 {
*/
public static final int FLAG_PLAYABLE = 1 << 1;
+ private final MediaItem2Provider mProvider;
+
/**
- * Create a new media item.
- *
- * @param metadata metadata with the media id.
- * @param flags The flags for this item.
+ * Create a new media item
+ * @hide
+ */
+ public MediaItem2(MediaItem2Provider provider) {
+ mProvider = provider;
+ }
+
+ /**
+ * @hide
*/
- public MediaItem2(@NonNull MediaMetadata2 metadata, @Flags int flags) {
- mFlags = flags;
- setMetadata(metadata);
+ public MediaItem2Provider getProvider() {
+ return mProvider;
}
/**
@@ -79,23 +77,22 @@ public class MediaItem2 {
* @return a new bundle instance
*/
public Bundle toBundle() {
- // TODO(jaewan): Fill here when we rebase.
- return new Bundle();
+ return mProvider.toBundle_impl();
+ }
+
+ public static MediaItem2 fromBundle(Bundle bundle) {
+ return ApiLoader.getProvider().fromBundle_MediaItem2(bundle);
}
public String toString() {
- final StringBuilder sb = new StringBuilder("MediaItem2{");
- sb.append("mFlags=").append(mFlags);
- sb.append(", mMetadata=").append(mMetadata);
- sb.append('}');
- return sb.toString();
+ return mProvider.toString_impl();
}
/**
* Gets the flags of the item.
*/
public @Flags int getFlags() {
- return mFlags;
+ return mProvider.getFlags_impl();
}
/**
@@ -103,7 +100,7 @@ public class MediaItem2 {
* @see #FLAG_BROWSABLE
*/
public boolean isBrowsable() {
- return (mFlags & FLAG_BROWSABLE) != 0;
+ return mProvider.isBrowsable_impl();
}
/**
@@ -111,36 +108,113 @@ public class MediaItem2 {
* @see #FLAG_PLAYABLE
*/
public boolean isPlayable() {
- return (mFlags & FLAG_PLAYABLE) != 0;
+ return mProvider.isPlayable_impl();
}
/**
- * Set a metadata. Metadata shouldn't be null and should have non-empty media id.
+ * Set a metadata. If the metadata is not null, its id should be matched with this instance's
+ * media id.
*
- * @param metadata
+ * @param metadata metadata to update
*/
- public void setMetadata(@NonNull MediaMetadata2 metadata) {
- if (metadata == null) {
- throw new IllegalArgumentException("metadata cannot be null");
- }
- if (TextUtils.isEmpty(metadata.getMediaId())) {
- throw new IllegalArgumentException("metadata must have a non-empty media id");
- }
- mMetadata = metadata;
+ public void setMetadata(@Nullable MediaMetadata2 metadata) {
+ mProvider.setMetadata_impl(metadata);
}
/**
* Returns the metadata of the media.
*/
- public @NonNull MediaMetadata2 getMetadata() {
- return mMetadata;
+ public @Nullable MediaMetadata2 getMetadata() {
+ return mProvider.getMetadata_impl();
}
/**
- * Returns the media id in the {@link MediaMetadata2} for this item.
- * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
+ * Returns the media id for this item.
*/
- public @Nullable String getMediaId() {
- return mMetadata.getMediaId();
+ 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();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return mProvider.equals_impl(obj);
+ }
+
+ /**
+ * Build {@link MediaItem2}
+ */
+ public static final class Builder {
+ private final MediaItem2Provider.BuilderProvider mProvider;
+
+ /**
+ * Constructor for {@link Builder}
+ *
+ * @param flags
+ */
+ public Builder(@Flags int flags) {
+ mProvider = ApiLoader.getProvider().createMediaItem2Builder(this, flags);
+ }
+
+ /**
+ * Set the media id of this instance. {@code null} for unset.
+ * <p>
+ * Media id is used to identify a media contents between session and controller.
+ * <p>
+ * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
+ * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
+ * used instead. If the id isn't set neither by {@link #setMediaId(String)} nor
+ * {@link #setMetadata(MediaMetadata2)}, id will be automatically generated.
+ *
+ * @param mediaId media id
+ * @return this instance for chaining
+ */
+ public Builder setMediaId(@Nullable String mediaId) {
+ return mProvider.setMediaId_impl(mediaId);
+ }
+
+ /**
+ * Set the metadata of this instance. {@code null} for unset.
+ * <p>
+ * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
+ * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
+ * used instead. If the id isn't set neither by {@link #setMediaId(String)} nor
+ * {@link #setMetadata(MediaMetadata2)}, id will be automatically generated.
+ *
+ * @param metadata metadata
+ * @return this instance for chaining
+ */
+ public Builder setMetadata(@Nullable MediaMetadata2 metadata) {
+ return mProvider.setMetadata_impl(metadata);
+ }
+
+ /**
+ * Set the data source descriptor for this instance. {@code null} for unset.
+ *
+ * @param dataSourceDesc data source descriptor
+ * @return this instance for chaining
+ */
+ public Builder setDataSourceDesc(@Nullable DataSourceDesc dataSourceDesc) {
+ return mProvider.setDataSourceDesc_impl(dataSourceDesc);
+ }
+
+ /**
+ * Build {@link MediaItem2}.
+ *
+ * @return a new {@link MediaItem2}.
+ */
+ public MediaItem2 build() {
+ return mProvider.build_impl();
+ }
}
}
diff --git a/android/media/MediaLibraryService2.java b/android/media/MediaLibraryService2.java
index d7e43ec9..f29d386c 100644
--- a/android/media/MediaLibraryService2.java
+++ b/android/media/MediaLibraryService2.java
@@ -20,27 +20,27 @@ import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
-import android.content.Context;
-import android.media.MediaSession2.BuilderBase;
+import android.media.MediaLibraryService2.MediaLibrarySession.Builder;
+import android.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
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.MediaSession2Provider;
import android.media.update.MediaSessionService2Provider;
import android.os.Bundle;
-import android.service.media.MediaBrowserService.BrowserRoot;
import java.util.List;
import java.util.concurrent.Executor;
/**
+ * @hide
* 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}.
+ * When extending this class, also add the following to your {@code AndroidManifest.xml}.
* <pre>
* &lt;service android:name="component_name_of_your_implementation" &gt;
* &lt;intent-filter&gt;
@@ -48,13 +48,13 @@ import java.util.concurrent.Executor;
* &lt;/intent-filter&gt;
* &lt;/service&gt;</pre>
* <p>
- * A {@link MediaLibraryService2} is extension of {@link MediaSessionService2}. IDs shouldn't
+ * The {@link MediaLibraryService2} class derives from {@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
+ *
+ * @see MediaSessionService2
*/
-// TODO(jaewan): Unhide
public abstract class MediaLibraryService2 extends MediaSessionService2 {
/**
* This is the interface name that a service implementing a session service should say that it
@@ -63,178 +63,252 @@ public abstract class MediaLibraryService2 extends MediaSessionService2 {
public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
/**
- * Session for the media library service.
+ * Session for the {@link MediaLibraryService2}. Build this object with
+ * {@link Builder} and return in {@link #onCreateSession(String)}.
*/
- public class MediaLibrarySession extends MediaSession2 {
+ public static final class MediaLibrarySession extends MediaSession2 {
private final MediaLibrarySessionProvider mProvider;
- MediaLibrarySession(Context context, MediaPlayerBase player, String id,
- Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
- int ratingType, PendingIntent sessionActivity) {
- super(context, player, id, callbackExecutor, callback, volumeProvider, ratingType,
- sessionActivity);
- mProvider = (MediaLibrarySessionProvider) getProvider();
- }
-
- @Override
- MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id,
- Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
- int ratingType, PendingIntent sessionActivity) {
- return ApiLoader.getProvider(context)
- .createMediaLibraryService2MediaLibrarySession(this, context, player, id,
- callbackExecutor, (MediaLibrarySessionCallback) callback,
- volumeProvider, ratingType, sessionActivity);
- }
-
/**
- * Notify subscribed controller about change in a parent's children.
- *
- * @param controller controller to notify
- * @param parentId
- * @param options
+ * Callback for the {@link MediaLibrarySession}.
*/
- public void notifyChildrenChanged(@NonNull ControllerInfo controller,
- @NonNull String parentId, @NonNull Bundle options) {
- mProvider.notifyChildrenChanged_impl(controller, parentId, options);
- }
+ public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
+ public MediaLibrarySessionCallback() {
+ super();
+ }
- /**
- * Notify subscribed controller about change in a parent's children.
- *
- * @param parentId parent id
- * @param options optional bundle
- */
- // This is for the backward compatibility.
- public void notifyChildrenChanged(@NonNull String parentId, @Nullable Bundle options) {
- mProvider.notifyChildrenChanged_impl(parentId, options);
- }
- }
+ /**
+ * 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 session the session for this event
+ * @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 onGetLibraryRoot(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controllerInfo, @Nullable Bundle rootHints) {
+ return null;
+ }
- public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
- /**
- * 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 browser 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 BrowserRoot} for accessing this app's content or null.
- * @see BrowserRoot#EXTRA_RECENT
- * @see BrowserRoot#EXTRA_OFFLINE
- * @see BrowserRoot#EXTRA_SUGGESTED
- */
- public @Nullable BrowserRoot 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 session the session for this event
+ * @param mediaId item id to get media item.
+ * @return a media item. {@code null} for no result or error.
+ */
+ public @Nullable MediaItem2 onGetItem(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controllerInfo, @NonNull String mediaId) {
+ 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 session the session for this event
+ * @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> onGetChildren(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId, int page,
+ int pageSize, @Nullable Bundle extras) {
+ return null;
+ }
+
+ /**
+ * Called when a controller subscribes to the parent.
+ * <p>
+ * It's your responsibility to keep subscriptions by your own and call
+ * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)}
+ * when the parent is changed.
+ *
+ * @param session the session for this event
+ * @param controller controller
+ * @param parentId parent id
+ * @param extras extra bundle
+ */
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ }
+
+ /**
+ * Called when a controller unsubscribes to the parent.
+ *
+ * @param session the session for this event
+ * @param controller controller
+ * @param parentId parent id
+ */
+ public void onUnsubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId) {
+ }
+
+ /**
+ * Called when a controller requests search.
+ *
+ * @param session the session for this event
+ * @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 MediaLibrarySession session,
+ @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 session the session for this event
+ * @param controllerInfo Information of the controller requesting the search result.
+ * @param query The search query which was previously sent through
+ * {@link #onSearch(MediaLibrarySession, ControllerInfo, String, Bundle)}.
+ * @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> onGetSearchResult(
+ @NonNull MediaLibrarySession session, @NonNull ControllerInfo controllerInfo,
+ @NonNull String query, int page, int pageSize, @Nullable Bundle extras) {
+ return null;
+ }
}
/**
- * Called to get the search result. Return search result here for the browser.
- * <p>
- * Return an empty list for no search result, and return {@code null} for the error.
- *
- * @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.
- * @return search result. {@code null} for error.
+ * Builder for {@link MediaLibrarySession}.
*/
- public @Nullable List<MediaItem2> onSearch(@NonNull ControllerInfo controllerInfo,
- @NonNull String query, @Nullable Bundle extras) {
- return null;
+ // 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<MediaLibrarySession, Builder,
+ MediaLibrarySessionCallback> {
+ // Builder requires MediaLibraryService2 instead of Context just to ensure that the
+ // builder can be only instantiated within the MediaLibraryService2.
+ // Ideally it's better to make it inner class of service to enforce, it violates API
+ // guideline that Builders should be the inner class of the building target.
+ public Builder(@NonNull MediaLibraryService2 service,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull MediaLibrarySessionCallback callback) {
+ super((instance) -> ApiLoader.getProvider().createMediaLibraryService2Builder(
+ service, (Builder) instance, callbackExecutor, callback));
+ }
+
+ @Override
+ public Builder setPlayer(@NonNull MediaPlayerBase player) {
+ return super.setPlayer(player);
+ }
+
+ @Override
+ public Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+ return super.setPlaylistAgent(playlistAgent);
+ }
+
+ @Override
+ public Builder setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) {
+ return super.setVolumeProvider(volumeProvider);
+ }
+
+ @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 @CallbackExecutor Executor executor,
+ @NonNull MediaLibrarySessionCallback callback) {
+ return super.setSessionCallback(executor, callback);
+ }
+
+ @Override
+ public MediaLibrarySession build() {
+ return super.build();
+ }
}
/**
- * Called to get the search result . Return result here for the browser.
- * <p>
- * Return an empty list for no search result, and return {@code null} for the error.
- *
- * @param itemId item id to get media item.
- * @return media item2. {@code null} for error.
+ * @hide
*/
- public @Nullable MediaItem2 onLoadItem(@NonNull ControllerInfo controllerInfo,
- @NonNull String itemId) {
- return null;
+ public MediaLibrarySession(MediaLibrarySessionProvider provider) {
+ super(provider);
+ mProvider = provider;
}
/**
- * Called to get the search result. Return search result here for the browser.
+ * Notify the controller of the change in a parent's children.
* <p>
- * Return an empty list for no search result, and return {@code null} for the error.
+ * If the controller hasn't subscribed to the parent, the API will do nothing.
+ * <p>
+ * Controllers will use {@link MediaBrowser2#getChildren(String, int, int, Bundle)} to get
+ * the list of children.
*
- * @param parentId parent id to get children
- * @param page number of page
- * @param pageSize size of the page
- * @param options
- * @return list of children. Can be {@code null}.
+ * @param controller controller to notify
+ * @param parentId parent id with changes in its children
+ * @param itemCount number of children.
+ * @param extras extra information from session to controller
*/
- public @Nullable List<MediaItem2> onLoadChildren(@NonNull ControllerInfo controller,
- @NonNull String parentId, int page, int pageSize, @Nullable Bundle options) {
- return null;
+ public void notifyChildrenChanged(@NonNull ControllerInfo controller,
+ @NonNull String parentId, int itemCount, @Nullable Bundle extras) {
+ mProvider.notifyChildrenChanged_impl(controller, parentId, itemCount, extras);
}
/**
- * Called when a controller subscribes to the parent.
+ * Notify all controllers that subscribed to the parent about change in the parent's
+ * children, regardless of the extra bundle supplied by
+ * {@link MediaBrowser2#subscribe(String, Bundle)}.
*
- * @param controller controller
* @param parentId parent id
- * @param options optional bundle
+ * @param itemCount number of children
+ * @param extras extra information from session to controller
*/
- public void onSubscribed(@NonNull ControllerInfo controller,
- String parentId, @Nullable Bundle options) {
+ // This is for the backward compatibility.
+ public void notifyChildrenChanged(@NonNull String parentId, int itemCount,
+ @Nullable Bundle extras) {
+ mProvider.notifyChildrenChanged_impl(parentId, itemCount, extras);
}
/**
- * Called when a controller unsubscribes to the parent.
+ * Notify controller about change in the search result.
*
- * @param controller controller
- * @param parentId parent id
- * @param options optional bundle
+ * @param controller controller to notify
+ * @param query previously sent search query from the controller.
+ * @param itemCount the number of items that have been found in the search.
+ * @param extras extra bundle
*/
- public void onUnsubscribed(@NonNull ControllerInfo controller,
- String parentId, @Nullable Bundle options) {
- }
- }
-
- /**
- * Builder for {@link MediaLibrarySession}.
- */
- // TODO(jaewan): Move this to updatable.
- public class MediaLibrarySessionBuilder
- extends BuilderBase<MediaLibrarySessionBuilder, MediaLibrarySessionCallback> {
- public MediaLibrarySessionBuilder(
- @NonNull Context context, @NonNull MediaPlayerBase player,
- @NonNull @CallbackExecutor Executor callbackExecutor,
- @NonNull MediaLibrarySessionCallback callback) {
- super(context, player);
- setSessionCallback(callbackExecutor, callback);
- }
-
- @Override
- public MediaLibrarySessionBuilder setSessionCallback(
- @NonNull @CallbackExecutor Executor callbackExecutor,
- @NonNull MediaLibrarySessionCallback callback) {
- if (callback == null) {
- throw new IllegalArgumentException("MediaLibrarySessionCallback cannot be null");
- }
- return super.setSessionCallback(callbackExecutor, callback);
- }
-
- @Override
- public MediaLibrarySession build() throws IllegalStateException {
- return new MediaLibrarySession(mContext, mPlayer, mId, mCallbackExecutor, mCallback,
- mVolumeProvider, mRatingType, mSessionActivity);
+ public void notifySearchResultChanged(@NonNull ControllerInfo controller,
+ @NonNull String query, int itemCount, @NonNull Bundle extras) {
+ mProvider.notifySearchResultChanged_impl(controller, query, itemCount, extras);
}
}
@Override
MediaSessionService2Provider createProvider() {
- return ApiLoader.getProvider(this).createMediaLibraryService2(this);
+ return ApiLoader.getProvider().createMediaLibraryService2(this);
}
/**
@@ -249,8 +323,8 @@ public abstract class MediaLibraryService2 extends MediaSessionService2 {
* This method will be called on the main thread.
*
* @param sessionId session id written in the AndroidManifest.xml.
- * @return a new browser session
- * @see MediaLibrarySessionBuilder
+ * @return a new library session
+ * @see Builder
* @see #getSession()
* @throws RuntimeException if returned session is invalid
*/
@@ -258,93 +332,91 @@ public abstract class MediaLibraryService2 extends MediaSessionService2 {
public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId);
/**
- * Contains information that the browser service needs to send to the client
- * when first connected.
+ * Contains information that the library service needs to send to the client when
+ * {@link MediaBrowser2#getLibraryRoot(Bundle)} is called.
*/
- public static final class BrowserRoot {
+ public static final class LibraryRoot {
/**
- * The lookup key for a boolean that indicates whether the browser service should return a
- * browser root for recently played media items.
+ * 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 browser service, this key can be
+ * <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 browser service can provide such media items, the implementation must return
+ * 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.
+ * {@link MediaLibrarySessionCallback#onGetLibraryRoot(MediaLibrarySession, 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.service.media.extra.RECENT";
+ public static final String EXTRA_RECENT = "android.media.extra.RECENT";
/**
- * The lookup key for a boolean that indicates whether the browser service should return a
- * browser root for offline media items.
+ * 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 browser service, this key can be
+ * <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 browser service can provide such media items, the implementation must return
+ * 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.
+ * {@link MediaLibrarySessionCallback#onGetLibraryRoot(MediaLibrarySession, 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.service.media.extra.OFFLINE";
+ public static final String EXTRA_OFFLINE = "android.media.extra.OFFLINE";
/**
- * The lookup key for a boolean that indicates whether the browser service should return a
- * browser root for suggested media items.
+ * 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 browser service, this key can be
- * supplied as a root hint for retrieving the media items suggested by the media browser
- * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
- * is considered ordered by relevance, first being the top suggestion.
- * If the media browser service can provide such media items, the implementation must return
+ * <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.
+ * {@link MediaLibrarySessionCallback#onGetLibraryRoot(MediaLibrarySession, 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.service.media.extra.SUGGESTED";
+ public static final String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED";
- final private String mRootId;
- final private Bundle mExtras;
+ private final LibraryRootProvider mProvider;
/**
- * Constructs a browser root.
+ * Constructs a library root.
* @param rootId The root id for browsing.
- * @param extras Any extras about the browser service.
+ * @param extras Any extras about the library service.
*/
- public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
- if (rootId == null) {
- throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
- "Use null for BrowserRoot instead.");
- }
- mRootId = rootId;
- mExtras = extras;
+ public LibraryRoot(@NonNull String rootId, @Nullable Bundle extras) {
+ mProvider = ApiLoader.getProvider().createMediaLibraryService2LibraryRoot(
+ this, rootId, extras);
}
/**
* Gets the root id for browsing.
*/
public String getRootId() {
- return mRootId;
+ return mProvider.getRootId_impl();
}
/**
- * Gets any extras about the browser service.
+ * Gets any extras about the library service.
*/
public Bundle getExtras() {
- return mExtras;
+ return mProvider.getExtras_impl();
}
}
}
diff --git a/android/media/MediaMetadata2.java b/android/media/MediaMetadata2.java
index 0e24db65..7b03ae0c 100644
--- a/android/media/MediaMetadata2.java
+++ b/android/media/MediaMetadata2.java
@@ -16,216 +16,348 @@
package android.media;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.graphics.Bitmap;
+import android.media.update.ApiLoader;
+import android.media.update.MediaMetadata2Provider;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.ArrayMap;
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
+ * Contains metadata about an item, such as the title, artist, etc.
*/
-// TODO(jaewan): Move this to updatable
+// New version of MediaMetadata with following changes
+// - Don't implement Parcelable for updatable support.
+// - Also support MediaDescription features. MediaDescription is deprecated instead because
+// it was insufficient for controller to display media contents.
public final class MediaMetadata2 {
- private static final String TAG = "MediaMetadata2";
-
/**
- * The title of the media.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the title of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
/**
- * The artist of the media.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the artist of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
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).
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * duration of the media in ms. A negative duration indicates that the duration is unknown
+ * (or infinite).
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
*/
public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
/**
- * The album title for the media.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the album title for the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
/**
- * The author of the media.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the author of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
/**
- * The writer of the media.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the writer of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
/**
- * The composer of the media.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the composer of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
/**
- * The compilation status of the media.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the compilation status of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
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.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the date the media was created or published.
+ * The format is unspecified but RFC 3339 is recommended.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
/**
- * The year the media was created or published as a long.
+ * The metadata key for a {@link Long} typed value to retrieve the information about the year
+ * the media was created or published.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
*/
public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
/**
- * The genre of the media.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the genre of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
/**
- * The track number for the media.
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * track number for the media.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
*/
public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
/**
- * The number of tracks in the media's original source.
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * number of tracks in the media's original source.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
*/
public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
/**
- * The disc number for the media's original source.
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * disc number for the media's original source.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
*/
public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
/**
- * The artist for the album of the media's original source.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the artist for the album of the media's original source.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
/**
- * The artwork for the media as a {@link Bitmap}.
+ * The metadata key for a {@link Bitmap} typed value to retrieve the information about the
+ * artwork for the media.
+ * 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.
*
- * 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.
+ * @see Builder#putBitmap(String, Bitmap)
+ * @see #getBitmap(String)
*/
public static final String METADATA_KEY_ART = "android.media.metadata.ART";
/**
- * The artwork for the media as a Uri style String.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about Uri of the artwork for the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(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.
+ * The metadata key for a {@link Bitmap} typed value to retrieve the information about the
+ * artwork for the album of the media's original source.
+ * 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.
+ *
+ * @see Builder#putBitmap(String, Bitmap)
+ * @see #getBitmap(String)
*/
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.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the Uri of the artwork for the album of the media's original source.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
/**
- * The user's rating for the media.
+ * The metadata key for a {@link Rating2} typed value to retrieve the information about the
+ * user's rating for the media.
*
- * @see Rating
+ * @see Builder#putRating(String, Rating2)
+ * @see #getRating(String)
*/
public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
/**
- * The overall rating for the media.
+ * The metadata key for a {@link Rating2} typed value to retrieve the information about the
+ * overall rating for the media.
*
- * @see Rating
+ * @see Builder#putRating(String, Rating2)
+ * @see #getRating(String)
*/
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.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the title that is suitable for display to the user.
+ * It 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
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
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
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.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
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 metadata key for a {@link Bitmap} typed value to retrieve the information about the 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.
+ * <p>
+ * 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.
*
- * 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.
+ * @see Builder#putBitmap(String, Bitmap)
+ * @see #getBitmap(String)
*/
- public static final String METADATA_KEY_DISPLAY_ICON
- = "android.media.metadata.DISPLAY_ICON";
+ 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
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the Uri of 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(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
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the media ID of 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
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(Uri, Bundle)}
- * to initiate playback when provided by a {@link MediaBrowser2} connected to
- * the same app.
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the Uri of the content. This value is specific to the service providing the
+ * content. It may be used with {@link MediaController2#playFromUri(Uri, Bundle)}
+ * to initiate playback when provided by a {@link MediaBrowser2} connected to the same app.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
*/
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
+ * The metadata key for a {@link Long} typed value to retrieve the information about 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>
@@ -236,6 +368,9 @@ public final class MediaMetadata2 {
* <li>{@link #BT_FOLDER_TYPE_PLAYLISTS}</li>
* <li>{@link #BT_FOLDER_TYPE_YEARS}</li>
* </ul>
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
*/
public static final String METADATA_KEY_BT_FOLDER_TYPE
= "android.media.metadata.BT_FOLDER_TYPE";
@@ -283,14 +418,19 @@ public final class MediaMetadata2 {
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.
+ * The metadata key for a {@link Long} typed value to retrieve the information about 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.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
*/
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
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * download status of the media which will be used for later offline playback. It should be
* one of the following:
*
* <ul>
@@ -298,6 +438,9 @@ public final class MediaMetadata2 {
* <li>{@link #STATUS_DOWNLOADING}</li>
* <li>{@link #STATUS_DOWNLOADED}</li>
* </ul>
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
*/
public static final String METADATA_KEY_DOWNLOAD_STATUS =
"android.media.metadata.DOWNLOAD_STATUS";
@@ -325,9 +468,8 @@ public final class MediaMetadata2 {
/**
* A {@link Bundle} extra.
- * @hide
*/
- public static final String METADATA_KEY_EXTRA = "android.media.metadata.EXTRA";
+ public static final String METADATA_KEY_EXTRAS = "android.media.metadata.EXTRAS";
/**
* @hide
@@ -364,76 +506,20 @@ public final class MediaMetadata2 {
@Retention(RetentionPolicy.SOURCE)
public @interface RatingKey {}
- static final int METADATA_TYPE_LONG = 0;
- static final int METADATA_TYPE_TEXT = 1;
- static final int METADATA_TYPE_BITMAP = 2;
- static final int METADATA_TYPE_RATING = 3;
- static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
-
- static {
- METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
- METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
- METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
- METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
- METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG);
- }
-
- private static final @TextKey String[] PREFERRED_DESCRIPTION_ORDER = {
- METADATA_KEY_TITLE,
- METADATA_KEY_ARTIST,
- METADATA_KEY_ALBUM,
- METADATA_KEY_ALBUM_ARTIST,
- METADATA_KEY_WRITER,
- METADATA_KEY_AUTHOR,
- METADATA_KEY_COMPOSER
- };
-
- private static final @BitmapKey String[] PREFERRED_BITMAP_ORDER = {
- METADATA_KEY_DISPLAY_ICON,
- METADATA_KEY_ART,
- METADATA_KEY_ALBUM_ART
- };
-
- private static final @TextKey String[] PREFERRED_URI_ORDER = {
- METADATA_KEY_DISPLAY_ICON_URI,
- METADATA_KEY_ART_URI,
- METADATA_KEY_ALBUM_ART_URI
- };
+ /**
+ * @hide
+ */
+ // TODO(jaewan): Add predefined float key.
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FloatKey {}
- final Bundle mBundle;
+ private final MediaMetadata2Provider mProvider;
/**
* @hide
*/
- public MediaMetadata2(Bundle bundle) {
- mBundle = new Bundle(bundle);
+ public MediaMetadata2(MediaMetadata2Provider provider) {
+ mProvider = provider;
}
/**
@@ -442,8 +528,8 @@ public final class MediaMetadata2 {
* @param key a String key
* @return true if the key exists in this metadata, false otherwise
*/
- public boolean containsKey(String key) {
- return mBundle.containsKey(key);
+ public boolean containsKey(@NonNull String key) {
+ return mProvider.containsKey_impl(key);
}
/**
@@ -454,20 +540,20 @@ public final class MediaMetadata2 {
* @param key The key the value is stored under
* @return a CharSequence value, or null
*/
- public CharSequence getText(@TextKey String key) {
- return mBundle.getCharSequence(key);
+ public @Nullable CharSequence getText(@NonNull @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.
+ * Returns the media id, or {@code null} if the id doesn't exist.
+ *<p>
+ * This is equivalent to the {@link #getString(String)} with the {@link #METADATA_KEY_MEDIA_ID}.
*
- * @
* @return media id. Can be {@code null}
+ * @see #METADATA_KEY_MEDIA_ID
*/
public @Nullable String getMediaId() {
- return getString(METADATA_KEY_MEDIA_ID);
+ return mProvider.getMediaId_impl();
}
/**
@@ -478,12 +564,8 @@ public final class MediaMetadata2 {
* @param key The key the value is stored under
* @return a String value, or null
*/
- public String getString(@TextKey String key) {
- CharSequence text = mBundle.getCharSequence(key);
- if (text != null) {
- return text.toString();
- }
- return null;
+ public @Nullable String getString(@NonNull @TextKey String key) {
+ return mProvider.getString_impl(key);
}
/**
@@ -493,27 +575,22 @@ public final class MediaMetadata2 {
* @param key The key the value is stored under
* @return a long value
*/
- public long getLong(@LongKey String key) {
- return mBundle.getLong(key, 0);
+ 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.
+ * <p>
+ * For the {@link #METADATA_KEY_USER_RATING}, A {@code null} return value means that user rating
+ * cannot be set by {@link MediaController2}.
*
* @param key The key the value is stored under
- * @return A {@link Rating2} or null
- */
- public Rating2 getRating(@RatingKey String key) {
- // TODO(jaewan): Add backward compatibility
- Rating2 rating = null;
- try {
- rating = Rating2.fromBundle(mBundle.getBundle(key));
- } catch (Exception e) {
- // ignore, value was not a rating
- Log.w(TAG, "Failed to retrieve a key as Rating.", e);
- }
- return rating;
+ * @return A {@link Rating2} or {@code null}
+ */
+ public @Nullable Rating2 getRating(@NonNull @RatingKey String key) {
+ return mProvider.getRating_impl(key);
}
/**
@@ -523,15 +600,19 @@ public final class MediaMetadata2 {
* @param key The key the value is stored under
* @return A {@link Bitmap} or null
*/
- public Bitmap getBitmap(@BitmapKey String key) {
- Bitmap bmp = null;
- try {
- bmp = mBundle.getParcelable(key);
- } catch (Exception e) {
- // ignore, value was not a bitmap
- Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
- }
- return bmp;
+ public @Nullable Bitmap getBitmap(@NonNull @BitmapKey String key) {
+ return mProvider.getBitmap_impl(key);
+ }
+
+ /**
+ * Return the value associated with the given key, or 0.0f if no long exists
+ * for the given key.
+ *
+ * @param key The key the value is stored under
+ * @return a float value
+ */
+ public float getFloat(@NonNull @FloatKey String key) {
+ return mProvider.getFloat_impl(key);
}
/**
@@ -539,14 +620,8 @@ public final class MediaMetadata2 {
*
* @return A {@link Bundle} or {@code null}
*/
- public Bundle getExtra() {
- try {
- return mBundle.getBundle(METADATA_KEY_EXTRA);
- } catch (Exception e) {
- // ignore, value was not an bundle
- Log.w(TAG, "Failed to retrieve an extra");
- }
- return null;
+ public @Nullable Bundle getExtras() {
+ return mProvider.getExtras_impl();
}
/**
@@ -555,7 +630,7 @@ public final class MediaMetadata2 {
* @return The number of fields in the metadata.
*/
public int size() {
- return mBundle.size();
+ return mProvider.size_impl();
}
/**
@@ -563,8 +638,8 @@ public final class MediaMetadata2 {
*
* @return a Set of String keys
*/
- public Set<String> keySet() {
- return mBundle.keySet();
+ public @NonNull Set<String> keySet() {
+ return mProvider.keySet_impl();
}
/**
@@ -573,8 +648,19 @@ public final class MediaMetadata2 {
*
* @return The Bundle backing this metadata.
*/
- public Bundle getBundle() {
- return mBundle;
+ public @NonNull Bundle toBundle() {
+ return mProvider.toBundle_impl();
+ }
+
+ /**
+ * Creates the {@link MediaMetadata2} from the bundle that previously returned by
+ * {@link #toBundle()}.
+ *
+ * @param bundle bundle for the metadata
+ * @return a new MediaMetadata2
+ */
+ public static @NonNull MediaMetadata2 fromBundle(@Nullable Bundle bundle) {
+ return ApiLoader.getProvider().fromBundle_MediaMetadata2(bundle);
}
/**
@@ -582,14 +668,14 @@ public final class MediaMetadata2 {
* use the appropriate data type.
*/
public static final class Builder {
- private final Bundle mBundle;
+ private final MediaMetadata2Provider.BuilderProvider mProvider;
/**
* Create an empty Builder. Any field that should be included in the
* {@link MediaMetadata2} must be added.
*/
public Builder() {
- mBundle = new Bundle();
+ mProvider = ApiLoader.getProvider().createMediaMetadata2Builder(this);
}
/**
@@ -599,31 +685,15 @@ public final class MediaMetadata2 {
*
* @param source
*/
- public Builder(MediaMetadata2 source) {
- mBundle = new Bundle(source.mBundle);
+ public Builder(@NonNull MediaMetadata2 source) {
+ mProvider = ApiLoader.getProvider().createMediaMetadata2Builder(this, source);
}
/**
- * Create a Builder using a {@link MediaMetadata2} instance to set
- * initial values, but replace bitmaps with a scaled down copy if they
- * are larger than maxBitmapSize.
- *
- * @param source The original metadata to copy.
- * @param maxBitmapSize The maximum height/width for bitmaps contained
- * in the metadata.
* @hide
*/
- public Builder(MediaMetadata2 source, int maxBitmapSize) {
- this(source);
- for (String key : mBundle.keySet()) {
- Object value = mBundle.get(key);
- if (value instanceof Bitmap) {
- Bitmap bmp = (Bitmap) value;
- if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
- putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
- }
- }
- }
+ public Builder(@NonNull MediaMetadata2Provider.BuilderProvider provider) {
+ mProvider = provider;
}
/**
@@ -652,15 +722,9 @@ public final class MediaMetadata2 {
* @param value The CharSequence value to store
* @return The Builder to allow chaining
*/
- public Builder putText(@TextKey String key, CharSequence value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a CharSequence");
- }
- }
- mBundle.putCharSequence(key, value);
- return this;
+ public @NonNull Builder putText(@NonNull @TextKey String key,
+ @Nullable CharSequence value) {
+ return mProvider.putText_impl(key, value);
}
/**
@@ -689,15 +753,9 @@ public final class MediaMetadata2 {
* @param value The String value to store
* @return The Builder to allow chaining
*/
- public Builder putString(@TextKey String key, String value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a String");
- }
- }
- mBundle.putCharSequence(key, value);
- return this;
+ public @NonNull Builder putString(@NonNull @TextKey String key,
+ @Nullable String value) {
+ return mProvider.putString_impl(key, value);
}
/**
@@ -719,15 +777,8 @@ public final class MediaMetadata2 {
* @param value The String value to store
* @return The Builder to allow chaining
*/
- public Builder putLong(@LongKey String key, long value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a long");
- }
- }
- mBundle.putLong(key, value);
- return this;
+ public @NonNull Builder putLong(@NonNull @LongKey String key, long value) {
+ return mProvider.putLong_impl(key, value);
}
/**
@@ -743,16 +794,8 @@ public final class MediaMetadata2 {
* @param value The String value to store
* @return The Builder to allow chaining
*/
- public Builder putRating(@RatingKey String key, Rating2 value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a Rating");
- }
- }
- mBundle.putBundle(key, value.toBundle());
-
- return this;
+ public @NonNull Builder putRating(@NonNull @RatingKey String key, @Nullable Rating2 value) {
+ return mProvider.putRating_impl(key, value);
}
/**
@@ -773,23 +816,29 @@ public final class MediaMetadata2 {
* @param value The Bitmap to store
* @return The Builder to allow chaining
*/
- public Builder putBitmap(@BitmapKey String key, Bitmap value) {
- if (METADATA_KEYS_TYPE.containsKey(key)) {
- if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
- throw new IllegalArgumentException("The " + key
- + " key cannot be used to put a Bitmap");
- }
- }
- mBundle.putParcelable(key, value);
- return this;
+ public @NonNull Builder putBitmap(@NonNull @BitmapKey String key, @Nullable Bitmap value) {
+ return mProvider.putBitmap_impl(key, value);
+ }
+
+ /**
+ * Put a float value into the metadata. Custom keys may be used.
+ *
+ * @param key The key for referencing this value
+ * @param value The float value to store
+ * @return The Builder to allow chaining
+ */
+ public @NonNull Builder putFloat(@NonNull @LongKey String key, float value) {
+ return mProvider.putFloat_impl(key, value);
}
/**
- * Set an extra {@link Bundle} into the metadata.
+ * Set a bundle of extras.
+ *
+ * @param extras The extras to include with this description or null.
+ * @return The Builder to allow chaining
*/
- public Builder setExtra(Bundle bundle) {
- mBundle.putBundle(METADATA_KEY_EXTRA, bundle);
- return this;
+ public Builder setExtras(@Nullable Bundle extras) {
+ return mProvider.setExtras_impl(extras);
}
/**
@@ -797,18 +846,8 @@ public final class MediaMetadata2 {
*
* @return The new MediaMetadata2 instance
*/
- public MediaMetadata2 build() {
- return new MediaMetadata2(mBundle);
- }
-
- private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
- float maxSizeF = maxSize;
- float widthScale = maxSizeF / bmp.getWidth();
- float heightScale = maxSizeF / bmp.getHeight();
- float scale = Math.min(widthScale, heightScale);
- int height = (int) (bmp.getHeight() * scale);
- int width = (int) (bmp.getWidth() * scale);
- return Bitmap.createScaledBitmap(bmp, width, height, true);
+ public @NonNull MediaMetadata2 build() {
+ return mProvider.build_impl();
}
}
}
diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java
index 745eb74d..0955dd63 100644
--- a/android/media/MediaMetadataRetriever.java
+++ b/android/media/MediaMetadataRetriever.java
@@ -17,6 +17,8 @@
package android.media;
import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
@@ -30,7 +32,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-
+import java.util.List;
import java.util.Map;
/**
@@ -367,27 +369,99 @@ public class MediaMetadataRetriever
private native Bitmap _getFrameAtTime(long timeUs, int option, int width, int height);
+ public static final class BitmapParams {
+ private Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;
+ private Bitmap.Config outActualConfig = Bitmap.Config.ARGB_8888;
+
+ /**
+ * Create a default BitmapParams object. By default, it uses {@link Bitmap.Config#ARGB_8888}
+ * as the preferred bitmap config.
+ */
+ public BitmapParams() {}
+
+ /**
+ * Set the preferred bitmap config for the decoder to decode into.
+ *
+ * If not set, or the request cannot be met, the decoder will output
+ * in {@link Bitmap.Config#ARGB_8888} config by default.
+ *
+ * After decode, the actual config used can be retrieved by {@link #getActualConfig()}.
+ *
+ * @param config the preferred bitmap config to use.
+ */
+ public void setPreferredConfig(@NonNull Bitmap.Config config) {
+ if (config == null) {
+ throw new IllegalArgumentException("preferred config can't be null");
+ }
+ inPreferredConfig = config;
+ }
+
+ /**
+ * Retrieve the preferred bitmap config in the params.
+ *
+ * @return the preferred bitmap config.
+ */
+ public @NonNull Bitmap.Config getPreferredConfig() {
+ return inPreferredConfig;
+ }
+
+ /**
+ * Get the actual bitmap config used to decode the bitmap after the decoding.
+ *
+ * @return the actual bitmap config used.
+ */
+ public @NonNull Bitmap.Config getActualConfig() {
+ return outActualConfig;
+ }
+ }
+
/**
* This method retrieves a video frame by its index. It should only be called
* after {@link #setDataSource}.
*
+ * After the bitmap is returned, you can query the actual parameters that were
+ * used to create the bitmap from the {@code BitmapParams} argument, for instance
+ * to query the bitmap config used for the bitmap with {@link BitmapParams#getActualConfig}.
+ *
* @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.
+ * @param params BitmapParams that controls the returned bitmap config (such as pixel formats).
*
* @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 #getFrameAtIndex(int)
+ * @see #getFramesAtIndex(int, int, BitmapParams)
+ * @see #getFramesAtIndex(int, int)
+ */
+ public Bitmap getFrameAtIndex(int frameIndex, @NonNull BitmapParams params) {
+ List<Bitmap> bitmaps = getFramesAtIndex(frameIndex, 1, params);
+ return bitmaps.get(0);
+ }
+
+ /**
+ * This method is similar to {@link #getFrameAtIndex(int, BitmapParams)} except that
+ * the default for {@link BitmapParams} will be used.
+ *
+ * @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 #getFrameAtIndex(int, BitmapParams)
+ * @see #getFramesAtIndex(int, int, BitmapParams)
* @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];
+ List<Bitmap> bitmaps = getFramesAtIndex(frameIndex, 1);
+ return bitmaps.get(0);
}
/**
@@ -395,7 +469,38 @@ public class MediaMetadataRetriever
* 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.
+ * this method is preferred over {@link #getFrameAtIndex(int, BitmapParams)} for efficiency.
+ *
+ * After the bitmaps are returned, you can query the actual parameters that were
+ * used to create the bitmaps from the {@code BitmapParams} argument, for instance
+ * to query the bitmap config used for the bitmaps with {@link BitmapParams#getActualConfig}.
+ *
+ * @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.
+ * @param params BitmapParams that controls the returned bitmap config (such as pixel formats).
+ *
+ * @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 list of Bitmaps containing the requested video frames. The returned
+ * array could contain less frames than requested if the retrieval fails.
+ *
+ * @see #getFrameAtIndex(int, BitmapParams)
+ * @see #getFrameAtIndex(int)
+ * @see #getFramesAtIndex(int, int)
+ */
+ public @NonNull List<Bitmap> getFramesAtIndex(
+ int frameIndex, int numFrames, @NonNull BitmapParams params) {
+ return getFramesAtIndexInternal(frameIndex, numFrames, params);
+ }
+
+ /**
+ * This method is similar to {@link #getFramesAtIndex(int, int, BitmapParams)} except that
+ * the default for {@link BitmapParams} will be used.
*
* @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
@@ -407,12 +512,19 @@ public class MediaMetadataRetriever
* @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
+ * @return An list of Bitmaps containing the requested video frames. The returned
* array could contain less frames than requested if the retrieval fails.
*
+ * @see #getFrameAtIndex(int, BitmapParams)
* @see #getFrameAtIndex(int)
+ * @see #getFramesAtIndex(int, int, BitmapParams)
*/
- public Bitmap[] getFramesAtIndex(int frameIndex, int numFrames) {
+ public @NonNull List<Bitmap> getFramesAtIndex(int frameIndex, int numFrames) {
+ return getFramesAtIndexInternal(frameIndex, numFrames, null);
+ }
+
+ private @NonNull List<Bitmap> getFramesAtIndexInternal(
+ int frameIndex, int numFrames, @Nullable BitmapParams params) {
if (!"yes".equals(extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO))) {
throw new IllegalStateException("Does not contail video or image sequences");
}
@@ -424,51 +536,107 @@ public class MediaMetadataRetriever
throw new IllegalArgumentException("Invalid frameIndex or numFrames: "
+ frameIndex + ", " + numFrames);
}
- return _getFrameAtIndex(frameIndex, numFrames);
+ return _getFrameAtIndex(frameIndex, numFrames, params);
}
- private native Bitmap[] _getFrameAtIndex(int frameIndex, int numFrames);
+
+ private native @NonNull List<Bitmap> _getFrameAtIndex(
+ int frameIndex, int numFrames, @Nullable BitmapParams params);
/**
* 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.
+ * After the bitmap is returned, you can query the actual parameters that were
+ * used to create the bitmap from the {@code BitmapParams} argument, for instance
+ * to query the bitmap config used for the bitmap with {@link BitmapParams#getActualConfig}.
+ *
+ * @param imageIndex 0-based index of the image.
+ * @param params BitmapParams that controls the returned bitmap config (such as pixel formats).
+ *
* @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
+ * @see #getImageAtIndex(int)
+ * @see #getPrimaryImage(BitmapParams)
+ * @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);
- }
+ public Bitmap getImageAtIndex(int imageIndex, @NonNull BitmapParams params) {
+ return getImageAtIndexInternal(imageIndex, params);
+ }
- return _getImageAtIndex(imageIndex);
+ /**
+ * This method is similar to {@link #getImageAtIndex(int, BitmapParams)} except that
+ * the default for {@link BitmapParams} will be used.
+ *
+ * @param imageIndex 0-based index of the 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 #getImageAtIndex(int, BitmapParams)
+ * @see #getPrimaryImage(BitmapParams)
+ * @see #getPrimaryImage()
+ */
+ public Bitmap getImageAtIndex(int imageIndex) {
+ return getImageAtIndexInternal(imageIndex, null);
}
/**
* This method retrieves the primary image of the media content. It should only
* be called after {@link #setDataSource}.
*
+ * After the bitmap is returned, you can query the actual parameters that were
+ * used to create the bitmap from the {@code BitmapParams} argument, for instance
+ * to query the bitmap config used for the bitmap with {@link BitmapParams#getActualConfig}.
+ *
+ * @param params BitmapParams that controls the returned bitmap config (such as pixel formats).
+ *
+ * @return the primary image, or null if it cannot be retrieved.
+ *
+ * @throws IllegalStateException if the container doesn't contain still images.
+ *
+ * @see #getImageAtIndex(int, BitmapParams)
+ * @see #getImageAtIndex(int)
+ * @see #getPrimaryImage()
+ */
+ public Bitmap getPrimaryImage(@NonNull BitmapParams params) {
+ return getImageAtIndexInternal(-1, params);
+ }
+
+ /**
+ * This method is similar to {@link #getPrimaryImage(BitmapParams)} except that
+ * the default for {@link BitmapParams} will be used.
+ *
* @return the primary image, or null if it cannot be retrieved.
*
* @throws IllegalStateException if the container doesn't contain still images.
*
+ * @see #getImageAtIndex(int, BitmapParams)
* @see #getImageAtIndex(int)
+ * @see #getPrimaryImage(BitmapParams)
*/
public Bitmap getPrimaryImage() {
- return getImageAtIndex(-1);
+ return getImageAtIndexInternal(-1, null);
}
- private native Bitmap _getImageAtIndex(int imageIndex);
+ private Bitmap getImageAtIndexInternal(int imageIndex, @Nullable BitmapParams params) {
+ 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, params);
+ }
+
+ private native Bitmap _getImageAtIndex(int imageIndex, @Nullable BitmapParams params);
/**
* Call this method after setDataSource(). This method finds the optional
@@ -712,7 +880,8 @@ public class MediaMetadataRetriever
public static final int METADATA_KEY_IMAGE_HEIGHT = 30;
/**
* If the media contains still images, this key retrieves the rotation
- * of the primary image.
+ * angle (in degrees clockwise) of the primary image. The image rotation
+ * angle must be one of 0, 90, 180, or 270 degrees.
*/
public static final int METADATA_KEY_IMAGE_ROTATION = 31;
/**
diff --git a/android/media/MediaMuxer.java b/android/media/MediaMuxer.java
index 02c71b28..205ce8d5 100644
--- a/android/media/MediaMuxer.java
+++ b/android/media/MediaMuxer.java
@@ -328,6 +328,7 @@ final public class MediaMuxer {
RandomAccessFile file = null;
try {
file = new RandomAccessFile(path, "rws");
+ file.setLength(0);
FileDescriptor fd = file.getFD();
setUpMediaMuxer(fd, format);
} finally {
diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java
index 1bc3dfa4..aef31b11 100644
--- a/android/media/MediaPlayer.java
+++ b/android/media/MediaPlayer.java
@@ -1484,6 +1484,7 @@ public class MediaPlayer extends PlayerBase
/*
* 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);
@@ -2415,7 +2416,7 @@ public class MediaPlayer extends PlayerBase
* Gets the track type.
* @return TrackType which indicates if the track is video, audio, timed text.
*/
- public int getTrackType() {
+ public @TrackType int getTrackType() {
return mTrackType;
}
@@ -2449,6 +2450,19 @@ public class MediaPlayer extends PlayerBase
public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
public static final int MEDIA_TRACK_TYPE_METADATA = 5;
+ /** @hide */
+ @IntDef(flag = false, prefix = "MEDIA_TRACK_TYPE", value = {
+ MEDIA_TRACK_TYPE_UNKNOWN,
+ MEDIA_TRACK_TYPE_VIDEO,
+ MEDIA_TRACK_TYPE_AUDIO,
+ MEDIA_TRACK_TYPE_TIMEDTEXT,
+ MEDIA_TRACK_TYPE_SUBTITLE,
+ MEDIA_TRACK_TYPE_METADATA }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TrackType {}
+
+
final int mTrackType;
final MediaFormat mFormat;
@@ -2599,26 +2613,30 @@ public class MediaPlayer extends PlayerBase
*/
/**
* MIME type for SubRip (SRT) container. Used in addTimedTextSource APIs.
+ * @deprecated use {@link MediaFormat#MIMETYPE_TEXT_SUBRIP}
*/
- public static final String MEDIA_MIMETYPE_TEXT_SUBRIP = "application/x-subrip";
+ public static final String MEDIA_MIMETYPE_TEXT_SUBRIP = MediaFormat.MIMETYPE_TEXT_SUBRIP;
/**
* MIME type for WebVTT subtitle data.
* @hide
+ * @deprecated
*/
- public static final String MEDIA_MIMETYPE_TEXT_VTT = "text/vtt";
+ public static final String MEDIA_MIMETYPE_TEXT_VTT = MediaFormat.MIMETYPE_TEXT_VTT;
/**
* MIME type for CEA-608 closed caption data.
* @hide
+ * @deprecated
*/
- public static final String MEDIA_MIMETYPE_TEXT_CEA_608 = "text/cea-608";
+ public static final String MEDIA_MIMETYPE_TEXT_CEA_608 = MediaFormat.MIMETYPE_TEXT_CEA_608;
/**
* MIME type for CEA-708 closed caption data.
* @hide
+ * @deprecated
*/
- public static final String MEDIA_MIMETYPE_TEXT_CEA_708 = "text/cea-708";
+ public static final String MEDIA_MIMETYPE_TEXT_CEA_708 = MediaFormat.MIMETYPE_TEXT_CEA_708;
/*
* A helper function to check if the mime type is supported by media framework.
@@ -3107,7 +3125,7 @@ public class MediaPlayer extends PlayerBase
* this function is called.
* </p>
* <p>
- * Currently, only timed text tracks or audio tracks can be selected via this method.
+ * Currently, only timed text, subtitle 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>
@@ -3794,29 +3812,158 @@ public class MediaPlayer extends PlayerBase
private OnTimedTextListener mOnTimedTextListener;
/**
- * Interface definition of a callback to be invoked when a
- * track has data available.
- *
- * @hide
+ * Interface definition of a callback to be invoked when a player subtitle track has new
+ * subtitle data available.
+ * See the {@link MediaPlayer#setOnSubtitleDataListener(OnSubtitleDataListener, Handler)}
+ * method for the description of which track will report data through this listener.
*/
- public interface OnSubtitleDataListener
- {
- public void onSubtitleData(MediaPlayer mp, SubtitleData data);
+ public interface OnSubtitleDataListener {
+ /**
+ * Method called when new subtitle data is available
+ * @param mp the player that reports the new subtitle data
+ * @param data the subtitle data
+ */
+ public void onSubtitleData(@NonNull MediaPlayer mp, @NonNull SubtitleData data);
}
/**
- * Register a callback to be invoked when a track has data available.
- *
- * @param listener the callback that will be run
- *
- * @hide
+ * Sets the listener to be invoked when a subtitle track has new data available.
+ * The subtitle data comes from a subtitle track previously selected with
+ * {@link #selectTrack(int)}. Use {@link #getTrackInfo()} to determine which tracks are
+ * subtitles (of type {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}), Subtitle track encodings
+ * can be determined by {@link TrackInfo#getFormat()}).<br>
+ * See {@link SubtitleData} for an example of querying subtitle encoding.
+ * @param listener the listener called when new data is available
+ * @param handler the {@link Handler} that receives the listener events
*/
- public void setOnSubtitleDataListener(OnSubtitleDataListener listener)
+ public void setOnSubtitleDataListener(@NonNull OnSubtitleDataListener listener,
+ @NonNull Handler handler) {
+ if (listener == null) {
+ throw new IllegalArgumentException("Illegal null listener");
+ }
+ if (handler == null) {
+ throw new IllegalArgumentException("Illegal null handler");
+ }
+ setOnSubtitleDataListenerInt(listener, handler);
+ }
+ /**
+ * Sets the listener to be invoked when a subtitle track has new data available.
+ * The subtitle data comes from a subtitle track previously selected with
+ * {@link #selectTrack(int)}. Use {@link #getTrackInfo()} to determine which tracks are
+ * subtitles (of type {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}), Subtitle track encodings
+ * can be determined by {@link TrackInfo#getFormat()}).<br>
+ * See {@link SubtitleData} for an example of querying subtitle encoding.<br>
+ * The listener will be called on the same thread as the one in which the MediaPlayer was
+ * created.
+ * @param listener the listener called when new data is available
+ */
+ public void setOnSubtitleDataListener(@NonNull OnSubtitleDataListener listener)
{
- mOnSubtitleDataListener = listener;
+ if (listener == null) {
+ throw new IllegalArgumentException("Illegal null listener");
+ }
+ setOnSubtitleDataListenerInt(listener, null);
+ }
+
+ /**
+ * Clears the listener previously set with
+ * {@link #setOnSubtitleDataListener(OnSubtitleDataListener)} or
+ * {@link #setOnSubtitleDataListener(OnSubtitleDataListener, Handler)}.
+ */
+ public void clearOnSubtitleDataListener() {
+ setOnSubtitleDataListenerInt(null, null);
+ }
+
+ private void setOnSubtitleDataListenerInt(
+ @Nullable OnSubtitleDataListener listener, @Nullable Handler handler) {
+ synchronized (this) {
+ mOnSubtitleDataListener = listener;
+ mOnSubtitleDataHandler = handler;
+ }
}
private OnSubtitleDataListener mOnSubtitleDataListener;
+ private Handler mOnSubtitleDataHandler;
+
+ /**
+ * Interface definition of a callback to be invoked when discontinuity in the normal progression
+ * of the media time is detected.
+ * The "normal progression" of media time is defined as the expected increase of the playback
+ * position when playing media, relative to the playback speed (for instance every second, media
+ * time increases by two seconds when playing at 2x).<br>
+ * Discontinuities are encountered in the following cases:
+ * <ul>
+ * <li>when the player is starved for data and cannot play anymore</li>
+ * <li>when the player encounters a playback error</li>
+ * <li>when the a seek operation starts, and when it's completed</li>
+ * <li>when the playback speed changes</li>
+ * <li>when the playback state changes</li>
+ * <li>when the player is reset</li>
+ * </ul>
+ * See the
+ * {@link MediaPlayer#setOnMediaTimeDiscontinuityListener(OnMediaTimeDiscontinuityListener, Handler)}
+ * method to set a listener for these events.
+ */
+ public interface OnMediaTimeDiscontinuityListener {
+ /**
+ * Called to indicate a time discontinuity has occured.
+ * @param mp the MediaPlayer for which the discontinuity has occured.
+ * @param mts the timestamp that correlates media time, system time and clock rate,
+ * or {@link MediaTimestamp#TIMESTAMP_UNKNOWN} in an error case.
+ */
+ public void onMediaTimeDiscontinuity(@NonNull MediaPlayer mp, @NonNull MediaTimestamp mts);
+ }
+
+ /**
+ * Sets the listener to be invoked when a media time discontinuity is encountered.
+ * @param listener the listener called after a discontinuity
+ * @param handler the {@link Handler} that receives the listener events
+ */
+ public void setOnMediaTimeDiscontinuityListener(
+ @NonNull OnMediaTimeDiscontinuityListener listener, @NonNull Handler handler) {
+ if (listener == null) {
+ throw new IllegalArgumentException("Illegal null listener");
+ }
+ if (handler == null) {
+ throw new IllegalArgumentException("Illegal null handler");
+ }
+ setOnMediaTimeDiscontinuityListenerInt(listener, handler);
+ }
+
+ /**
+ * Sets the listener to be invoked when a media time discontinuity is encountered.
+ * The listener will be called on the same thread as the one in which the MediaPlayer was
+ * created.
+ * @param listener the listener called after a discontinuity
+ */
+ public void setOnMediaTimeDiscontinuityListener(
+ @NonNull OnMediaTimeDiscontinuityListener listener)
+ {
+ if (listener == null) {
+ throw new IllegalArgumentException("Illegal null listener");
+ }
+ setOnMediaTimeDiscontinuityListenerInt(listener, null);
+ }
+
+ /**
+ * Clears the listener previously set with
+ * {@link #setOnMediaTimeDiscontinuityListener(OnMediaTimeDiscontinuityListener)}
+ * or {@link #setOnMediaTimeDiscontinuityListener(OnMediaTimeDiscontinuityListener, Handler)}
+ */
+ public void clearOnMediaTimeDiscontinuityListener() {
+ setOnMediaTimeDiscontinuityListenerInt(null, null);
+ }
+
+ private void setOnMediaTimeDiscontinuityListenerInt(
+ @Nullable OnMediaTimeDiscontinuityListener listener, @Nullable Handler handler) {
+ synchronized (this) {
+ mOnMediaTimeDiscontinuityListener = listener;
+ mOnMediaTimeDiscontinuityHandler = handler;
+ }
+ }
+
+ private OnMediaTimeDiscontinuityListener mOnMediaTimeDiscontinuityListener;
+ private Handler mOnMediaTimeDiscontinuityHandler;
/**
* Interface definition of a callback to be invoked when a
@@ -3952,8 +4099,8 @@ public class MediaPlayer extends PlayerBase
/** The player was started because it was used as the next player for another
* player, which just completed playback.
+ * @see android.media.MediaPlayer#setNextMediaPlayer(MediaPlayer)
* @see android.media.MediaPlayer.OnInfoListener
- * @hide
*/
public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
diff --git a/android/media/MediaPlayer2.java b/android/media/MediaPlayer2.java
index d36df845..dcc872c1 100644
--- a/android/media/MediaPlayer2.java
+++ b/android/media/MediaPlayer2.java
@@ -28,32 +28,22 @@ 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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.Executor;
/**
+ * @hide
* 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}.
@@ -91,27 +81,18 @@ import java.util.UUID;
* <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
+ * <li>When a MediaPlayer2 object is just created using <code>create</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
+ * <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 #setPlayerVolume(float)}, {@link #pause()}, {@link #play()},
* {@link #seekTo(long, int)} 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>
+ * {@link #prepare()} 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
@@ -135,9 +116,9 @@ import java.util.UUID;
* 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
+ * MediaPlayer2EventCallback.onError() method if an MediaPlayer2EventCallback has been
* registered beforehand via
- * {@link #registerEventCallback(Executor, EventCallback)}.
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
* <ul>
* <li>It is important to note that once an error occurs, the
* MediaPlayer2 object enters the <em>Error</em> state (except as noted
@@ -151,43 +132,42 @@ import java.util.UUID;
* 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>
+ * {@link #prepare()}, {@link #setDataSource(DataSourceDesc)}
+ * methods in an invalid state. </li>
* </ul>
* </li>
* <li>Calling
- * {@link #setDataSource(DataSourceDesc)}, or
- * {@code setPlaylist} transfers a
+ * {@link #setDataSource(DataSourceDesc)} 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>
+ * setDataSource() 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>
+ * <code>setDataSource</code>.</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
+ * a call to {@link #prepare()} (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>
+ * onInfo() of the MediaPlayer2EventCallback interface with {@link #MEDIA_INFO_PREPARED},
+ * if an MediaPlayer2EventCallback is registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</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
+ * thrown if {@link #prepare()} 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
@@ -196,13 +176,14 @@ import java.util.UUID;
* </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
+ * <em>Started</em> state. {@link #getPlayerState()} 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)}.
+ * a user supplied callback method MediaPlayer2EventCallback.onInfo() with
+ * {@link #MEDIA_INFO_BUFFERING_UPDATE} if an MediaPlayer2EventCallback has been
+ * registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
* This callback allows applications to keep track of the buffering status
* while streaming audio/video.</li>
* <li>Calling {@link #play()} has not effect
@@ -215,7 +196,7 @@ import java.util.UUID;
* <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
+ * the state is updated in calls to {@link #getPlayerState()}, 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
@@ -234,9 +215,10 @@ import java.util.UUID;
* 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>
+ * supplied MediaPlayer2EventCallback.onCallCompleted() with
+ * {@link #CALL_COMPLETED_SEEK_TO}
+ * if an MediaPlayer2EventCallback has been registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</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
@@ -252,15 +234,13 @@ import java.util.UUID;
* </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 current source is set to loop by {@link #loopCurrent(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)}.
+ * MediaPlayer2EventCallback.onCompletion(), if an MediaPlayer2EventCallback is
+ * registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
* 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>
@@ -280,7 +260,7 @@ import java.util.UUID;
* <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.
+ * <td>This method must be called after setDataSource.
* Calling it does not change the object state. </p></td></tr>
* <tr><td>getAudioSessionId </p></td>
* <td>any </p></td>
@@ -314,7 +294,7 @@ import java.util.UUID;
* <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>
+ * <tr><td>getPlayerState </p></td>
* <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
* PlaybackCompleted}</p></td>
* <td>{Error}</p></td>
@@ -327,7 +307,7 @@ import java.util.UUID;
* <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>
+ * <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
@@ -354,13 +334,13 @@ import java.util.UUID;
* <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>
+ * prepare().</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
+ * calling setDataSource. Calling it does not change the object
* state. </p></td></tr>
* <tr><td>setAudioStreamType (deprecated)</p></td>
* <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
@@ -368,7 +348,7 @@ import java.util.UUID;
* <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>
+ * prepare().</p></td></tr>
* <tr><td>setAuxEffectSendLevel </p></td>
* <td>any</p></td>
* <td>{} </p></td>
@@ -380,13 +360,6 @@ import java.util.UUID;
* <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>
@@ -397,7 +370,7 @@ import java.util.UUID;
* <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>
+ * <tr><td>loopCurrent </p></td>
* <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
* PlaybackCompleted}</p></td>
* <td>{Error}</p></td>
@@ -409,12 +382,12 @@ import java.util.UUID;
* <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>
+ * <tr><td>setDrmEventCallback </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>
+ * <tr><td>setMediaPlayer2EventCallback </p></td>
* <td>any </p></td>
* <td>{} </p></td>
* <td>This method can be called in any state and calling it does not change
@@ -424,7 +397,7 @@ import java.util.UUID;
* <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>
+ * <tr><td>setPlayerVolume </p></td>
* <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
* PlaybackCompleted}</p></td>
* <td>{Error}</p></td>
@@ -472,66 +445,338 @@ import java.util.UUID;
* 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)}).
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)},
+ * {@link #setDrmEventCallback(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
-{
+public abstract class MediaPlayer2 extends MediaPlayerBase
+ implements SubtitleController.Listener
+ , AudioRouting {
/**
- Constant to retrieve only the new metadata since the last
- call.
- // FIXME: unhide.
- // FIXME: add link to getMetadata(boolean, boolean)
- {@hide}
+ * Create a MediaPlayer2 object.
+ *
+ * @return A MediaPlayer2 object created
*/
- public static final boolean METADATA_UPDATE_ONLY = true;
+ public static final MediaPlayer2 create() {
+ // TODO: load MediaUpdate APK
+ return new MediaPlayer2Impl();
+ }
+
+ private static final String[] decodeMediaPlayer2Uri(String location) {
+ Uri uri = Uri.parse(location);
+ if (!"mediaplayer2".equals(uri.getScheme())) {
+ return new String[] {location};
+ }
+
+ List<String> uris = uri.getQueryParameters("uri");
+ if (uris.isEmpty()) {
+ return new String[] {location};
+ }
+
+ List<String> keys = uri.getQueryParameters("key");
+ List<String> values = uri.getQueryParameters("value");
+ if (keys.size() != values.size()) {
+ return new String[] {uris.get(0)};
+ }
+
+ List<String> ls = new ArrayList();
+ ls.add(uris.get(0));
+ for (int i = 0; i < keys.size() ; i++) {
+ ls.add(keys.get(i));
+ ls.add(values.get(i));
+ }
+
+ return ls.toArray(new String[ls.size()]);
+ }
+
+ private static final String encodeMediaPlayer2Uri(String uri, String[] keys, String[] values) {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme("mediaplayer2").path("/").appendQueryParameter("uri", uri);
+ if (keys == null || values == null || keys.length != values.length) {
+ return builder.build().toString();
+ }
+ for (int i = 0; i < keys.length ; i++) {
+ builder
+ .appendQueryParameter("key", keys[i])
+ .appendQueryParameter("value", values[i]);
+ }
+ return builder.build().toString();
+ }
/**
- Constant to retrieve all the metadata.
- // FIXME: unhide.
- // FIXME: add link to getMetadata(boolean, boolean)
- {@hide}
+ * @hide
*/
- public static final boolean METADATA_ALL = false;
+ // add hidden empty constructor so it doesn't show in SDK
+ public MediaPlayer2() { }
/**
- Constant to enable the metadata filter during retrieval.
- // FIXME: unhide.
- // FIXME: add link to getMetadata(boolean, boolean)
- {@hide}
+ * 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.
*/
- public static final boolean APPLY_METADATA_FILTER = true;
+ // This is a synchronous call.
+ @Override
+ public abstract void close();
/**
- Constant to disable the metadata filter during retrieval.
- // FIXME: unhide.
- // FIXME: add link to getMetadata(boolean, boolean)
- {@hide}
+ * Starts or resumes playback. If playback had previously been paused,
+ * playback will continue from where it was paused. If playback had
+ * reached end of stream and been paused, or never started before,
+ * playback will start at the beginning. If the source had not been
+ * prepared, the player will prepare the source and play.
+ *
*/
- public static final boolean BYPASS_METADATA_FILTER = false;
+ // This is an asynchronous call.
+ @Override
+ public abstract void play();
/**
- * Create a MediaPlayer2 object.
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to
+ * call prepare().
*
- * @return A MediaPlayer2 object created
*/
- public static final MediaPlayer2 create() {
- // TODO: load MediaUpdate APK
- return new MediaPlayer2Impl();
+ // This is an asynchronous call.
+ @Override
+ public abstract void prepare();
+
+ /**
+ * Pauses playback. Call play() to resume.
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void pause();
+
+ /**
+ * Tries to play next data source if applicable.
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void skipToNext();
+
+ /**
+ * Moves the media to specified time position.
+ * Same as {@link #seekTo(long, int)} with {@code mode = SEEK_PREVIOUS_SYNC}.
+ *
+ * @param msec the offset in milliseconds from the start to seek to
+ */
+ // This is an asynchronous call.
+ @Override
+ public void seekTo(long msec) {
+ seekTo(msec, SEEK_PREVIOUS_SYNC /* mode */);
}
/**
- * @hide
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
*/
- // add hidden empty constructor so it doesn't show in SDK
- public MediaPlayer2() { }
+ @Override
+ public abstract long 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 abstract long getDuration();
+
+ /**
+ * Gets the current buffered media source position 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.
+ *
+ * @return the current buffered media source position in milliseconds
+ */
+ @Override
+ public abstract long getBufferedPosition();
+
+ /**
+ * Gets the current player state.
+ *
+ * @return the current player state.
+ */
+ @Override
+ public abstract @PlayerState int getPlayerState();
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ * @return the buffering state, one of the following:
+ */
+ @Override
+ public abstract @BuffState int getBufferingState();
+
+ /**
+ * 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()} in order
+ * for the audio attributes to become effective thereafter.
+ * @param attributes a non-null set of audio attributes
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setAudioAttributes(@NonNull AudioAttributes attributes);
+
+ /**
+ * Gets the audio attributes for this MediaPlayer2.
+ * @return attributes a set of audio attributes
+ */
+ @Override
+ public abstract @Nullable AudioAttributes getAudioAttributes();
+
+ /**
+ * Sets the data source as described by a DataSourceDesc.
+ *
+ * @param dsd the descriptor of data source you want to play
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setDataSource(@NonNull DataSourceDesc dsd);
+
+ /**
+ * Sets a single data source as described by a DataSourceDesc which will be played
+ * after current data source is finished.
+ *
+ * @param dsd the descriptor of data source you want to play after current one
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
+
+ /**
+ * Sets a list of data sources to be played sequentially after current data source is done.
+ *
+ * @param dsds the list of data sources you want to play after current one
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
+
+ /**
+ * Gets the current data source as described by a DataSourceDesc.
+ *
+ * @return the current DataSourceDesc
+ */
+ @Override
+ public abstract @NonNull DataSourceDesc getCurrentDataSource();
+
+ /**
+ * Configures the player to loop on the current data source.
+ * @param loop true if the current data source is meant to loop.
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void loopCurrent(boolean loop);
+
+ /**
+ * Sets the playback speed.
+ * A value of 1.0f is the default playback value.
+ * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+ * before using negative values.<br>
+ * After changing the playback speed, it is recommended to query the actual speed supported
+ * by the player, see {@link #getPlaybackSpeed()}.
+ * @param speed the desired playback speed
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setPlaybackSpeed(float speed);
+
+ /**
+ * Returns the actual playback speed to be used by the player when playing.
+ * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+ * @return the actual playback speed
+ */
+ @Override
+ public float getPlaybackSpeed() {
+ return 1.0f;
+ }
+
+ /**
+ * Indicates whether reverse playback is supported.
+ * Reverse playback is indicated by negative playback speeds, see
+ * {@link #setPlaybackSpeed(float)}.
+ * @return true if reverse playback is supported.
+ */
+ @Override
+ public boolean isReversePlaybackSupported() {
+ return false;
+ }
+
+ /**
+ * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+ * on the audio samples.
+ * Note that this volume is specific to the player, and is separate from stream volume
+ * used across the platform.<br>
+ * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+ * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+ * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setPlayerVolume(float volume);
+
+ /**
+ * Returns the current volume of this player to this player.
+ * Note that it does not take into account the associated stream volume.
+ * @return the player volume.
+ */
+ @Override
+ public abstract float getPlayerVolume();
+
+ /**
+ * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+ */
+ @Override
+ public float getMaxPlayerVolume() {
+ return 1.0f;
+ }
+
+ /**
+ * Adds a callback to be notified of events for this player.
+ * @param e the {@link Executor} to be used for the events.
+ * @param cb the callback to receive the events.
+ */
+ // This is a synchronous call.
+ @Override
+ public abstract void registerPlayerEventCallback(@NonNull Executor e,
+ @NonNull PlayerEventCallback cb);
+
+ /**
+ * Removes a previously registered callback for player events
+ * @param cb the callback to remove
+ */
+ // This is a synchronous call.
+ @Override
+ public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
/**
* Create a request parcel which can be routed to the native media
@@ -552,7 +797,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* 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
+ * Must be called after setDataSource to make sure a native player
* exists. On failure, a RuntimeException is thrown.
*
* @param request Parcel with the data for the extension. The
@@ -565,6 +810,20 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
public void invoke(Parcel request, Parcel reply) { }
/**
+ * Insert a task in the command queue to help the client to identify whether a batch
+ * of commands has been finished. When this command is processed, a notification
+ * {@code MediaPlayer2EventCallback.onCommandLabelReached} will be fired with the
+ * given {@code label}.
+ *
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCommandLabelReached
+ *
+ * @param label An application specific Object used to help to identify the completeness
+ * of a batch of commands.
+ */
+ // This is an asynchronous call.
+ public void notifyWhenCommandLabelReached(@NonNull Object label) { }
+
+ /**
* Sets the {@link SurfaceHolder} to use for displaying the video
* portion of the media.
*
@@ -600,6 +859,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @throws IllegalStateException if the internal player engine has not been
* initialized or has been released.
*/
+ // This is an asynchronous call.
public abstract void setSurface(Surface surface);
/* Do not change these video scaling mode values below without updating
@@ -648,204 +908,10 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
/**
* Discards all pending commands.
*/
+ // This is a synchronous call.
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
@@ -854,14 +920,6 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
*/
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
//--------------------
@@ -874,6 +932,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and
* does not correspond to a valid audio device.
*/
+ // This is an asynchronous call.
@Override
public abstract boolean setPreferredDevice(AudioDeviceInfo deviceInfo);
@@ -901,6 +960,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @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.
*/
+ // This is a synchronous call.
@Override
public abstract void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener,
Handler handler);
@@ -911,6 +971,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface
* to remove.
*/
+ // This is a synchronous call.
@Override
public abstract void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener);
@@ -949,9 +1010,10 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
*
* @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.
+ * yet. The {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+ * is available.
*/
public abstract int getVideoWidth();
@@ -960,9 +1022,9 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
*
* @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.
+ * yet. The {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height is available.
*/
public abstract int getVideoHeight();
@@ -984,10 +1046,65 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @return true if currently playing, false otherwise
* @throws IllegalStateException if the internal player engine has not been
* initialized or has been released.
+ * @hide
*/
public abstract boolean isPlaying();
/**
+ * MediaPlayer2 has not been prepared or just has been reset.
+ * In this state, MediaPlayer2 doesn't fetch data.
+ * @hide
+ */
+ public static final int MEDIAPLAYER2_STATE_IDLE = 1;
+
+ /**
+ * MediaPlayer2 has been just prepared.
+ * In this state, MediaPlayer2 just fetches data from media source,
+ * but doesn't actively render data.
+ * @hide
+ */
+ public static final int MEDIAPLAYER2_STATE_PREPARED = 2;
+
+ /**
+ * MediaPlayer2 is paused.
+ * In this state, MediaPlayer2 doesn't actively render data.
+ * @hide
+ */
+ public static final int MEDIAPLAYER2_STATE_PAUSED = 3;
+
+ /**
+ * MediaPlayer2 is actively playing back data.
+ * @hide
+ */
+ public static final int MEDIAPLAYER2_STATE_PLAYING = 4;
+
+ /**
+ * MediaPlayer2 has hit some fatal error and cannot continue playback.
+ * @hide
+ */
+ public static final int MEDIAPLAYER2_STATE_ERROR = 5;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, prefix = "MEDIAPLAYER2_STATE", value = {
+ MEDIAPLAYER2_STATE_IDLE,
+ MEDIAPLAYER2_STATE_PREPARED,
+ MEDIAPLAYER2_STATE_PAUSED,
+ MEDIAPLAYER2_STATE_PLAYING,
+ MEDIAPLAYER2_STATE_ERROR })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MediaPlayer2State {}
+
+ /**
+ * Gets the current MediaPlayer2 state.
+ *
+ * @return the current MediaPlayer2 state.
+ * @hide
+ */
+ public abstract @MediaPlayer2State int getMediaPlayer2State();
+
+ /**
* 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.
@@ -1016,6 +1133,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @throws IllegalArgumentException if params is invalid or not supported.
* @hide
*/
+ // This is an asynchronous call.
public void setBufferingParams(@NonNull BufferingParams params) { }
/**
@@ -1060,8 +1178,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
public static final int PLAYBACK_RATE_AUDIO_MODE_DEFAULT = 0;
/** @hide */
- @IntDef(
- value = {
+ @IntDef(flag = false, prefix = "PLAYBACK_RATE_AUDIO_MODE", value = {
PLAYBACK_RATE_AUDIO_MODE_DEFAULT,
PLAYBACK_RATE_AUDIO_MODE_STRETCH,
PLAYBACK_RATE_AUDIO_MODE_RESAMPLE,
@@ -1097,19 +1214,14 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* 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.
*/
+ // This is an asynchronous call.
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();
@@ -1118,20 +1230,14 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* 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.
*/
+ // This is an asynchronous call.
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();
@@ -1177,8 +1283,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
public static final int SEEK_CLOSEST = 0x03;
/** @hide */
- @IntDef(
- value = {
+ @IntDef(flag = false, prefix = "SEEK", value = {
SEEK_PREVIOUS_SYNC,
SEEK_NEXT_SYNC,
SEEK_CLOSEST_SYNC,
@@ -1203,20 +1308,8 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* 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.
*/
+ // This is an asynchronous call.
public abstract void seekTo(long msec, @SeekMode int mode);
/**
@@ -1241,21 +1334,6 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
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
@@ -1300,31 +1378,12 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
}
/**
- * 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().
+ * data source and calling prepare().
*/
+ // This is a synchronous call.
+ @Override
public abstract void reset();
/**
@@ -1338,24 +1397,6 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
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
@@ -1366,31 +1407,6 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
}
/**
- * 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.
@@ -1404,9 +1420,8 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* 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.
*/
+ // This is an asynchronous call.
public abstract void setAudioSessionId(int sessionId);
/**
@@ -1431,6 +1446,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* methods.
* @param effectId system wide unique id of the effect to attach
*/
+ // This is an asynchronous call.
public abstract void attachAuxEffect(int effectId);
@@ -1446,6 +1462,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* 0 < x <= R -> level = 10^(72*(x-R)/20/R)
* @param level send level scalar
*/
+ // This is an asynchronous call.
public abstract void setAuxEffectSendLevel(float level);
/**
@@ -1494,7 +1511,6 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @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();
@@ -1661,6 +1677,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
*
* @see android.media.MediaPlayer2#getTrackInfo
*/
+ // This is an asynchronous call.
public abstract void selectTrack(int index);
/**
@@ -1677,63 +1694,9 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
*
* @see android.media.MediaPlayer2#getTrackInfo
*/
+ // This is an asynchronous call.
public abstract void deselectTrack(int index);
- /**
- * Sets the target UDP re-transmit endpoint for the low level player.
- * Generally, the address portion of the endpoint is an IP multicast
- * address, although a unicast address would be equally valid. When a valid
- * retransmit endpoint has been set, the media player will not decode and
- * render the media presentation locally. Instead, the player will attempt
- * to re-multiplex its media data using the Android@Home RTP profile and
- * re-transmit to the target endpoint. Receiver devices (which may be
- * either the same as the transmitting device or different devices) may
- * instantiate, prepare, and start a receiver player using a setDataSource
- * URL of the form...
- *
- * aahRX://&lt;multicastIP&gt;:&lt;port&gt;
- *
- * to receive, decode and render the re-transmitted content.
- *
- * setRetransmitEndpoint may only be called before setDataSource has been
- * called; while the player is in the Idle state.
- *
- * @param endpoint the address and UDP port of the re-transmission target or
- * null if no re-transmission is to be performed.
- * @throws IllegalStateException if it is called in an invalid state
- * @throws IllegalArgumentException if the retransmit endpoint is supplied,
- * but invalid.
- *
- * {@hide} pending API council
- */
- public void setRetransmitEndpoint(InetSocketAddress endpoint) { }
-
- /**
- * 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;
@@ -1743,22 +1706,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* 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) { }
-
+ public abstract static class MediaPlayer2EventCallback {
/**
* Called to indicate the video size
*
@@ -1766,22 +1714,22 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* 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 dsd the DataSourceDesc 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) { }
+ public void onVideoSizeChanged(MediaPlayer2 mp, DataSourceDesc dsd, 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 dsd the DataSourceDesc 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) { }
+ public void onTimedText(MediaPlayer2 mp, DataSourceDesc dsd, TimedText text) { }
/**
* Called to indicate avaliable timed metadata
@@ -1798,82 +1746,82 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @see TimedMetaData
*
* @param mp the MediaPlayer2 associated with this callback
- * @param srcId the Id of this data source
+ * @param dsd the DataSourceDesc of this data source
* @param data the timed metadata sample associated with this event
*/
- public void onTimedMetaDataAvailable(MediaPlayer2 mp, long srcId, TimedMetaData data) { }
+ public void onTimedMetaDataAvailable(
+ MediaPlayer2 mp, DataSourceDesc dsd, 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 dsd the DataSourceDesc of this data source
+ * @param what the type of error that has occurred.
* @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) { }
+ public void onError(
+ MediaPlayer2 mp, DataSourceDesc dsd, @MediaError 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 dsd the DataSourceDesc 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) { }
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, @MediaInfo int what, int extra) { }
+
+ /**
+ * Called to acknowledge an API call.
+ *
+ * @param mp the MediaPlayer2 the call was made on.
+ * @param dsd the DataSourceDesc of this data source
+ * @param what the enum for the API call.
+ * @param status the returned status code for the call.
+ */
+ public void onCallCompleted(
+ MediaPlayer2 mp, DataSourceDesc dsd, @CallCompleted int what,
+ @CallStatus int status) { }
+
+ /**
+ * Called to indicate media clock has changed.
+ *
+ * @param mp the MediaPlayer2 the media time pertains to.
+ * @param dsd the DataSourceDesc of this data source
+ * @param timestamp the new media clock.
+ */
+ public void onMediaTimeChanged(
+ MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) { }
+
+ /**
+ * Called to indicate {@link #notifyWhenCommandLabelReached(Object)} has been processed.
+ *
+ * @param mp the MediaPlayer2 {@link #notifyWhenCommandLabelReached(Object)} was called on.
+ * @param label the application specific Object given by
+ * {@link #notifyWhenCommandLabelReached(Object)}.
+ */
+ public void onCommandLabelReached(MediaPlayer2 mp, @NonNull Object label) { }
}
/**
- * Register a callback to be invoked when the media source is ready
- * for playback.
+ * Sets the 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);
+ // This is a synchronous call.
+ public abstract void setMediaPlayer2EventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull MediaPlayer2EventCallback eventCallback);
/**
- * Unregisters an {@link EventCallback}.
- *
- * @param callback an {@link EventCallback} to unregister
+ * Clears the {@link MediaPlayer2EventCallback}.
*/
- public abstract void unregisterEventCallback(EventCallback callback);
+ // This is a synchronous call.
+ public abstract void clearMediaPlayer2EventCallback();
/**
* Interface definition of a callback to be invoked when a
@@ -1893,6 +1841,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
*
* @hide
*/
+ // This is a synchronous call.
public void setOnSubtitleDataListener(OnSubtitleDataListener listener) { }
@@ -1900,14 +1849,14 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* in include/media/mediaplayer2.h!
*/
/** Unspecified media player error.
- * @see android.media.MediaPlayer2.EventCallback.onError
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onError
*/
public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200;
@@ -1923,106 +1872,117 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
/** Unspecified low-level system error. This value originated from UNKNOWN_ERROR in
* system/core/include/utils/Errors.h
- * @see android.media.MediaPlayer2.EventCallback.onError
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onError
* @hide
*/
public static final int MEDIA_ERROR_SYSTEM = -2147483648;
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, prefix = "MEDIA_ERROR", value = {
+ MEDIA_ERROR_UNKNOWN,
+ MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+ MEDIA_ERROR_IO,
+ MEDIA_ERROR_MALFORMED,
+ MEDIA_ERROR_UNSUPPORTED,
+ MEDIA_ERROR_TIMED_OUT,
+ MEDIA_ERROR_SYSTEM
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MediaError {}
/* Do not change these values without updating their counterparts
* in include/media/mediaplayer2.h!
*/
/** Unspecified media player info.
- * @see android.media.MediaPlayer2.EventCallback.onInfo
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * next-to-be-played in the playlist.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ /** The player just completed the playback of the full playlist.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
*/
public static final int MEDIA_INFO_BUFFERING_START = 701;
/** MediaPlayer2 is resuming playback after filling buffers.
- * @see android.media.MediaPlayer2.EventCallback.onInfo
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
* @hide
*/
public static final int MEDIA_INFO_NETWORK_BANDWIDTH = 703;
+ /**
+ * 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.
+ *
+ * The {@code extra} parameter in {@code MediaPlayer2EventCallback.onInfo} is the
+ * percentage (0-100) of the content that has been buffered or played thus far.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_BUFFERING_UPDATE = 704;
+
/** 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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
*/
public static final int MEDIA_INFO_NOT_SEEKABLE = 801;
/** A new set of metadata is available.
- * @see android.media.MediaPlayer2.EventCallback.onInfo
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
*/
public static final int MEDIA_INFO_METADATA_UPDATE = 802;
@@ -2034,33 +1994,273 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
/** Informs that audio is not playing. Note that playback of the video
* is not interrupted.
- * @see android.media.MediaPlayer2.EventCallback.onInfo
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
*/
public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805;
/** Failed to handle timed text track properly.
- * @see android.media.MediaPlayer2.EventCallback.onInfo
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#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
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
*/
public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901;
/** Reading the subtitle track takes too long.
- * @see android.media.MediaPlayer2.EventCallback.onInfo
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
*/
public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902;
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, prefix = "MEDIA_INFO", value = {
+ MEDIA_INFO_UNKNOWN,
+ MEDIA_INFO_STARTED_AS_NEXT,
+ MEDIA_INFO_VIDEO_RENDERING_START,
+ MEDIA_INFO_AUDIO_RENDERING_START,
+ MEDIA_INFO_PLAYBACK_COMPLETE,
+ MEDIA_INFO_PLAYLIST_END,
+ MEDIA_INFO_PREPARED,
+ MEDIA_INFO_VIDEO_TRACK_LAGGING,
+ MEDIA_INFO_BUFFERING_START,
+ MEDIA_INFO_BUFFERING_END,
+ MEDIA_INFO_NETWORK_BANDWIDTH,
+ MEDIA_INFO_BUFFERING_UPDATE,
+ MEDIA_INFO_BAD_INTERLEAVING,
+ MEDIA_INFO_NOT_SEEKABLE,
+ MEDIA_INFO_METADATA_UPDATE,
+ MEDIA_INFO_EXTERNAL_METADATA_UPDATE,
+ MEDIA_INFO_AUDIO_NOT_PLAYING,
+ MEDIA_INFO_VIDEO_NOT_PLAYING,
+ MEDIA_INFO_TIMED_TEXT_ERROR,
+ MEDIA_INFO_UNSUPPORTED_SUBTITLE,
+ MEDIA_INFO_SUBTITLE_TIMED_OUT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MediaInfo {}
+
+ //--------------------------------------------------------------------------
+ /** The player just completed a call {@link #attachAuxEffect}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_ATTACH_AUX_EFFECT = 1;
+
+ /** The player just completed a call {@link #deselectTrack}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_DESELECT_TRACK = 2;
+
+ /** The player just completed a call {@link #loopCurrent}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_LOOP_CURRENT = 3;
+
+ /** The player just completed a call {@link #pause}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_PAUSE = 4;
+
+ /** The player just completed a call {@link #play}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_PLAY = 5;
+
+ /** The player just completed a call {@link #prepare}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_PREPARE = 6;
+
+ /** The player just completed a call {@link #releaseDrm}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_RELEASE_DRM = 12;
+
+ /** The player just completed a call {@link #restoreDrmKeys}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_RESTORE_DRM_KEYS = 13;
+
+ /** The player just completed a call {@link #seekTo}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SEEK_TO = 14;
+
+ /** The player just completed a call {@link #selectTrack}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SELECT_TRACK = 15;
+
+ /** The player just completed a call {@link #setAudioAttributes}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_AUDIO_ATTRIBUTES = 16;
+
+ /** The player just completed a call {@link #setAudioSessionId}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_AUDIO_SESSION_ID = 17;
+
+ /** The player just completed a call {@link #setAuxEffectSendLevel}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL = 18;
+
+ /** The player just completed a call {@link #setDataSource}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_DATA_SOURCE = 19;
+
+ /** The player just completed a call {@link #setNextDataSource}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCE = 22;
+
+ /** The player just completed a call {@link #setNextDataSources}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCES = 23;
+
+ /** The player just completed a call {@link #setPlaybackParams}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_PLAYBACK_PARAMS = 24;
+
+ /** The player just completed a call {@link #setPlaybackSpeed}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_PLAYBACK_SPEED = 25;
+
+ /** The player just completed a call {@link #setPlayerVolume}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_PLAYER_VOLUME = 26;
+
+ /** The player just completed a call {@link #setSurface}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_SURFACE = 27;
+
+ /** The player just completed a call {@link #setSyncParams}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_SYNC_PARAMS = 28;
+
+ /** The player just completed a call {@link #skipToNext}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SKIP_TO_NEXT = 29;
+
+ /** The player just completed a call {@link #setBufferingParams}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ * @hide
+ */
+ public static final int CALL_COMPLETED_SET_BUFFERING_PARAMS = 1001;
+
+ /** The player just completed a call {@code setVideoScalingMode}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ * @hide
+ */
+ public static final int CALL_COMPLETED_SET_VIDEO_SCALING_MODE = 1002;
+
+ /** The player just completed a call {@code notifyWhenCommandLabelReached}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCommandLabelReached
+ * @hide
+ */
+ public static final int CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED = 1003;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, prefix = "CALL_COMPLETED", value = {
+ CALL_COMPLETED_ATTACH_AUX_EFFECT,
+ CALL_COMPLETED_DESELECT_TRACK,
+ CALL_COMPLETED_LOOP_CURRENT,
+ CALL_COMPLETED_PAUSE,
+ CALL_COMPLETED_PLAY,
+ CALL_COMPLETED_PREPARE,
+ CALL_COMPLETED_RELEASE_DRM,
+ CALL_COMPLETED_RESTORE_DRM_KEYS,
+ CALL_COMPLETED_SEEK_TO,
+ CALL_COMPLETED_SELECT_TRACK,
+ CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
+ CALL_COMPLETED_SET_AUDIO_SESSION_ID,
+ CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL,
+ CALL_COMPLETED_SET_DATA_SOURCE,
+ CALL_COMPLETED_SET_NEXT_DATA_SOURCE,
+ CALL_COMPLETED_SET_NEXT_DATA_SOURCES,
+ CALL_COMPLETED_SET_PLAYBACK_PARAMS,
+ CALL_COMPLETED_SET_PLAYBACK_SPEED,
+ CALL_COMPLETED_SET_PLAYER_VOLUME,
+ CALL_COMPLETED_SET_SURFACE,
+ CALL_COMPLETED_SET_SYNC_PARAMS,
+ CALL_COMPLETED_SKIP_TO_NEXT,
+ CALL_COMPLETED_SET_BUFFERING_PARAMS,
+ CALL_COMPLETED_SET_VIDEO_SCALING_MODE,
+ CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CallCompleted {}
+
+ /** Status code represents that call is completed without an error.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_NO_ERROR = 0;
+
+ /** Status code represents that call is ended with an unknown error.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_ERROR_UNKNOWN = Integer.MIN_VALUE;
+
+ /** Status code represents that the player is not in valid state for the operation.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_INVALID_OPERATION = 1;
+
+ /** Status code represents that the argument is illegal.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_BAD_VALUE = 2;
+
+ /** Status code represents that the operation is not allowed.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_PERMISSION_DENIED = 3;
+
+ /** Status code represents a file or network related operation error.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_ERROR_IO = 4;
+
+ /** Status code represents that DRM operation is called before preparing a DRM scheme through
+ * {@link #prepareDrm}.
+ * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_NO_DRM_SCHEME = 5;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, prefix = "CALL_STATUS", value = {
+ CALL_STATUS_NO_ERROR,
+ CALL_STATUS_ERROR_UNKNOWN,
+ CALL_STATUS_INVALID_OPERATION,
+ CALL_STATUS_BAD_VALUE,
+ CALL_STATUS_PERMISSION_DENIED,
+ CALL_STATUS_ERROR_IO,
+ CALL_STATUS_NO_DRM_SCHEME})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CallStatus {}
// Modular DRM begin
@@ -2071,8 +2271,8 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* '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}.
+ * The only allowed DRM calls in this listener are {@link #getDrmPropertyString}
+ * and {@link #setDrmPropertyString}.
*/
public interface OnDrmConfigHelper
{
@@ -2080,8 +2280,9 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* Called to give the app the opportunity to configure DRM before the session is created
*
* @param mp the {@code MediaPlayer2} associated with this callback
+ * @param dsd the DataSourceDesc of this data source
*/
- public void onDrmConfig(MediaPlayer2 mp);
+ public void onDrmConfig(MediaPlayer2 mp, DataSourceDesc dsd);
}
/**
@@ -2092,6 +2293,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
*
* @param listener the callback that will be run
*/
+ // This is a synchronous call.
public abstract void setOnDrmConfigHelper(OnDrmConfigHelper listener);
/**
@@ -2102,42 +2304,40 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
/**
* 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
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param dsd the DataSourceDesc of this data source
+ * @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) { }
+ public void onDrmInfo(MediaPlayer2 mp, DataSourceDesc dsd, DrmInfo drmInfo) { }
/**
- * Called to notify the client that {@code prepareDrm} is finished and ready for key request/response.
+ * Called to notify the client that {@link #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}.
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param dsd the DataSourceDesc of this data source
+ * @param status the result of DRM preparation.
*/
- public void onDrmPrepared(MediaPlayer2 mp, @PrepareDrmStatusCode int status) { }
-
+ public void onDrmPrepared(
+ MediaPlayer2 mp, DataSourceDesc dsd, @PrepareDrmStatusCode int status) { }
}
/**
- * Register a callback to be invoked when the media source is ready
- * for playback.
+ * Sets the 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,
+ // This is a synchronous call.
+ public abstract void setDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull DrmEventCallback eventCallback);
/**
- * Unregisters a {@link DrmEventCallback}.
- *
- * @param callback a {@link DrmEventCallback} to unregister
+ * Clears the {@link DrmEventCallback}.
*/
- public abstract void unregisterDrmEventCallback(DrmEventCallback callback);
+ // This is a synchronous call.
+ public abstract void clearDrmEventCallback();
/**
* The status codes for {@link DrmEventCallback#onDrmPrepared} listener.
@@ -2164,7 +2364,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
/** @hide */
- @IntDef({
+ @IntDef(flag = false, prefix = "PREPARE_DRM_STATUS", value = {
PREPARE_DRM_STATUS_SUCCESS,
PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
@@ -2183,10 +2383,10 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
/**
* Prepares the DRM for the current source
* <p>
- * If {@code OnDrmConfigHelper} is registered, it will be called during
+ * If {@link 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}
+ * {@link #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
@@ -2216,6 +2416,7 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @throws ProvisioningServerErrorException if provisioning is required but failed due to
* the request denied by the provisioning server
*/
+ // This is a synchronous call.
public abstract void prepareDrm(@NonNull UUID uuid)
throws UnsupportedSchemeException, ResourceBusyException,
ProvisioningNetworkErrorException, ProvisioningServerErrorException;
@@ -2229,20 +2430,21 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
*
* @throws NoDrmSchemeException if there is no active DRM session to release
*/
+ // This is an asynchronous call.
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
+ * getDrmKeyRequest() 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}.
+ * {@link #provideDrmKeyResponse}.
*
* @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
@@ -2269,22 +2471,23 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @throws NoDrmSchemeException if there is no active DRM session
*/
@NonNull
- public abstract MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
+ public abstract MediaDrm.KeyRequest getDrmKeyRequest(
+ @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
+ * provided to the DRM engine plugin using provideDrmKeyResponse. 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}.
+ * {@ link # restoreDrmKeys}.
* 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
+ * passed to the earlier {@ link # getDrmKeyRequest} 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
@@ -2293,16 +2496,19 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* @throws DeniedByServerException if the response indicates that the
* server rejected the request
*/
- public abstract byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+ // This is a synchronous call.
+ public abstract byte[] provideDrmKeyResponse(
+ @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}.
+ * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
*
* @param keySetId identifies the saved key set to restore
*/
- public abstract void restoreKeys(@NonNull byte[] keySetId)
+ // This is an asynchronous call.
+ public abstract void restoreDrmKeys(@NonNull byte[] keySetId)
throws NoDrmSchemeException;
/**
@@ -2315,7 +2521,8 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
*/
@NonNull
- public abstract String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName)
+ public abstract String getDrmPropertyString(
+ @NonNull @MediaDrm.StringProperty String propertyName)
throws NoDrmSchemeException;
/**
@@ -2328,8 +2535,9 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
* {@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)
+ // This is a synchronous call.
+ public abstract void setDrmPropertyString(
+ @NonNull @MediaDrm.StringProperty String propertyName, @NonNull String value)
throws NoDrmSchemeException;
/**
@@ -2473,4 +2681,38 @@ public abstract class MediaPlayer2 implements SubtitleController.Listener
public static final String ERROR_CODE = "android.media.mediaplayer.errcode";
}
+
+ /**
+ 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;
+
}
diff --git a/android/media/MediaPlayer2Impl.java b/android/media/MediaPlayer2Impl.java
index 86a285cc..56423fda 100644
--- a/android/media/MediaPlayer2Impl.java
+++ b/android/media/MediaPlayer2Impl.java
@@ -17,7 +17,6 @@
package android.media;
import android.annotation.CallbackExecutor;
-import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
@@ -25,8 +24,10 @@ import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
+import android.graphics.SurfaceTexture;
+import android.media.SubtitleController.Anchor;
+import android.media.SubtitleTrack.RenderingWidget;
import android.net.Uri;
-import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
@@ -34,31 +35,19 @@ 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.Process;
import android.os.SystemProperties;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
+import android.util.ArrayMap;
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.AudioManager;
-import android.media.MediaDrm;
-import android.media.MediaFormat;
-import android.media.MediaPlayer2;
-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 com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
@@ -74,485 +63,26 @@ import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.lang.AutoCloseable;
-import java.lang.Runnable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
-import java.net.CookieHandler;
-import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
-import java.net.InetSocketAddress;
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.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.UUID;
import java.util.Vector;
-
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
/**
- * 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 {
@@ -572,18 +102,27 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
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;
+ private final Object mSrcLock = new Object();
+ //--- guarded by |mSrcLock| start
+ private long mSrcIdGenerator = 0;
+ private DataSourceDesc mCurrentDSD;
+ private long mCurrentSrcId = mSrcIdGenerator++;
+ private List<DataSourceDesc> mNextDSDs;
+ private long mNextSrcId = mSrcIdGenerator++;
+ private int mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ private boolean mNextSourcePlayPending = false;
+ //--- guarded by |mSrcLock| end
+
+ private AtomicInteger mBufferedPercentageCurrent = new AtomicInteger(0);
+ private AtomicInteger mBufferedPercentageNext = new AtomicInteger(0);
+ private volatile float mVolume = 1.0f;
// Modular DRM
- private UUID mDrmUUID;
private final Object mDrmLock = new Object();
+ //--- guarded by |mDrmLock| start
+ private UUID mDrmUUID;
private DrmInfoImpl mDrmInfoImpl;
private MediaDrm mDrmObj;
private byte[] mDrmSessionId;
@@ -593,6 +132,15 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
private boolean mDrmProvisioningInProgress;
private boolean mPrepareDrmInProgress;
private ProvisioningThread mDrmProvisioningThread;
+ //--- guarded by |mDrmLock| end
+
+ private HandlerThread mHandlerThread;
+ private final Handler mTaskHandler;
+ private final Object mTaskLock = new Object();
+ @GuardedBy("mTaskLock")
+ private final List<Task> mPendingTasks = new LinkedList<>();
+ @GuardedBy("mTaskLock")
+ private Task mCurrentTask;
/**
* Default constructor.
@@ -610,6 +158,11 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
mEventHandler = null;
}
+ mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
+ mHandlerThread.start();
+ looper = mHandlerThread.getLooper();
+ mTaskHandler = new Handler(looper);
+
mTimeProvider = new TimeProvider(this);
mOpenSubtitleSources = new Vector<InputStream>();
mGuard.open("close");
@@ -620,6 +173,436 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
native_setup(new WeakReference<MediaPlayer2Impl>(this));
}
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * 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() {
+ addTask(new Task(CALL_COMPLETED_PLAY, false) {
+ @Override
+ void process() {
+ stayAwake(true);
+ _start();
+ }
+ });
+ }
+
+ private native void _start() throws IllegalStateException;
+
+ /**
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare(). For streams, you should call prepare(),
+ * which returns immediately, rather than blocking until enough data has been
+ * buffered.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void prepare() {
+ addTask(new Task(CALL_COMPLETED_PREPARE, true) {
+ @Override
+ void process() {
+ _prepare();
+ }
+ });
+ }
+
+ public native void _prepare();
+
+ /**
+ * Pauses playback. Call play() to resume.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ public void pause() {
+ addTask(new Task(CALL_COMPLETED_PAUSE, false) {
+ @Override
+ void process() {
+ stayAwake(false);
+ _pause();
+ }
+ });
+ }
+
+ private native void _pause() throws IllegalStateException;
+
+ /**
+ * Tries to play next data source if applicable.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void skipToNext() {
+ addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
+ @Override
+ void process() {
+ // TODO: switch to next data source and play
+ }
+ });
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
+ */
+ @Override
+ public native long 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 long getDuration();
+
+ /**
+ * Gets the current buffered media source position 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.
+ *
+ * @return the current buffered media source position in milliseconds
+ */
+ @Override
+ public long getBufferedPosition() {
+ // Use cached buffered percent for now.
+ return getDuration() * mBufferedPercentageCurrent.get() / 100;
+ }
+
+ @Override
+ public @PlayerState int getPlayerState() {
+ int mediaplayer2State = getMediaPlayer2State();
+ int playerState;
+ switch (mediaplayer2State) {
+ case MEDIAPLAYER2_STATE_IDLE:
+ playerState = PLAYER_STATE_IDLE;
+ break;
+ case MEDIAPLAYER2_STATE_PREPARED:
+ case MEDIAPLAYER2_STATE_PAUSED:
+ playerState = PLAYER_STATE_PAUSED;
+ break;
+ case MEDIAPLAYER2_STATE_PLAYING:
+ playerState = PLAYER_STATE_PLAYING;
+ break;
+ case MEDIAPLAYER2_STATE_ERROR:
+ default:
+ playerState = PLAYER_STATE_ERROR;
+ break;
+ }
+
+ return playerState;
+ }
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ */
+ @Override
+ public @BuffState int getBufferingState() {
+ // TODO: use cached state or call native function.
+ return BUFFERING_STATE_UNKNOWN;
+ }
+
+ /**
+ * 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()} 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(@NonNull AudioAttributes attributes) {
+ addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
+ @Override
+ void process() {
+ if (attributes == null) {
+ final String msg = "Cannot set AudioAttributes to null";
+ throw new IllegalArgumentException(msg);
+ }
+ Parcel pattributes = Parcel.obtain();
+ attributes.writeToParcel(pattributes, AudioAttributes.FLATTEN_TAGS);
+ setParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES, pattributes);
+ pattributes.recycle();
+ }
+ });
+ }
+
+ @Override
+ public @NonNull AudioAttributes getAudioAttributes() {
+ Parcel pattributes = getParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES);
+ AudioAttributes attributes = AudioAttributes.CREATOR.createFromParcel(pattributes);
+ pattributes.recycle();
+ return attributes;
+ }
+
+ /**
+ * 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) {
+ addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ // TODO: setDataSource could update exist data source
+ synchronized (mSrcLock) {
+ mCurrentDSD = dsd;
+ mCurrentSrcId = mSrcIdGenerator++;
+ try {
+ handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId);
+ } catch (IOException e) {
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets a single data source as described by a DataSourceDesc which will be played
+ * after current data source is finished.
+ *
+ * @param dsd the descriptor of data source you want to play after current one
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void setNextDataSource(@NonNull DataSourceDesc dsd) {
+ addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ synchronized (mSrcLock) {
+ mNextDSDs = new ArrayList<DataSourceDesc>(1);
+ mNextDSDs.add(dsd);
+ mNextSrcId = mSrcIdGenerator++;
+ mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ mNextSourcePlayPending = false;
+ }
+ int state = getMediaPlayer2State();
+ if (state != MEDIAPLAYER2_STATE_IDLE) {
+ synchronized (mSrcLock) {
+ prepareNextDataSource_l();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets a list of data sources to be played sequentially after current data source is done.
+ *
+ * @param dsds the list of data sources you want to play after current one
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if dsds is null or empty, or contains null DataSourceDesc
+ */
+ @Override
+ public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
+ addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCES, false) {
+ @Override
+ void process() {
+ if (dsds == null || dsds.size() == 0) {
+ throw new IllegalArgumentException("data source list cannot be null or empty.");
+ }
+ for (DataSourceDesc dsd : dsds) {
+ if (dsd == null) {
+ throw new IllegalArgumentException(
+ "DataSourceDesc in the source list cannot be null.");
+ }
+ }
+
+ synchronized (mSrcLock) {
+ mNextDSDs = new ArrayList(dsds);
+ mNextSrcId = mSrcIdGenerator++;
+ mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ mNextSourcePlayPending = false;
+ }
+ int state = getMediaPlayer2State();
+ if (state != MEDIAPLAYER2_STATE_IDLE) {
+ synchronized (mSrcLock) {
+ prepareNextDataSource_l();
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public @NonNull DataSourceDesc getCurrentDataSource() {
+ synchronized (mSrcLock) {
+ return mCurrentDSD;
+ }
+ }
+
+ /**
+ * Configures the player to loop on the current data source.
+ * @param loop true if the current data source is meant to loop.
+ */
+ @Override
+ public void loopCurrent(boolean loop) {
+ addTask(new Task(CALL_COMPLETED_LOOP_CURRENT, false) {
+ @Override
+ void process() {
+ // TODO: set the looping mode, send notification
+ setLooping(loop);
+ }
+ });
+ }
+
+ private native void setLooping(boolean looping);
+
+ /**
+ * Sets the playback speed.
+ * A value of 1.0f is the default playback value.
+ * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+ * before using negative values.<br>
+ * After changing the playback speed, it is recommended to query the actual speed supported
+ * by the player, see {@link #getPlaybackSpeed()}.
+ * @param speed the desired playback speed
+ */
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_SPEED, false) {
+ @Override
+ void process() {
+ _setPlaybackParams(getPlaybackParams().setSpeed(speed));
+ }
+ });
+ }
+
+ /**
+ * Returns the actual playback speed to be used by the player when playing.
+ * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+ * @return the actual playback speed
+ */
+ @Override
+ public float getPlaybackSpeed() {
+ return getPlaybackParams().getSpeed();
+ }
+
+ /**
+ * Indicates whether reverse playback is supported.
+ * Reverse playback is indicated by negative playback speeds, see
+ * {@link #setPlaybackSpeed(float)}.
+ * @return true if reverse playback is supported.
+ */
+ @Override
+ public boolean isReversePlaybackSupported() {
+ return false;
+ }
+
+ /**
+ * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+ * on the audio samples.
+ * Note that this volume is specific to the player, and is separate from stream volume
+ * used across the platform.<br>
+ * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+ * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+ * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+ */
+ @Override
+ public void setPlayerVolume(float volume) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
+ @Override
+ void process() {
+ mVolume = volume;
+ _setVolume(volume, volume);
+ }
+ });
+ }
+
+ private native void _setVolume(float leftVolume, float rightVolume);
+
+ /**
+ * Returns the current volume of this player to this player.
+ * Note that it does not take into account the associated stream volume.
+ * @return the player volume.
+ */
+ @Override
+ public float getPlayerVolume() {
+ return mVolume;
+ }
+
+ /**
+ * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+ */
+ @Override
+ public float getMaxPlayerVolume() {
+ return 1.0f;
+ }
+
+ /**
+ * Adds a callback to be notified of events for this player.
+ * @param e the {@link Executor} to be used for the events.
+ * @param cb the callback to receive the events.
+ */
+ @Override
+ public void registerPlayerEventCallback(@NonNull Executor e,
+ @NonNull PlayerEventCallback cb) {
+ }
+
+ /**
+ * Removes a previously registered callback for player events
+ * @param cb the callback to remove
+ */
+ @Override
+ public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
+ }
+
+
+ private static final int NEXT_SOURCE_STATE_ERROR = -1;
+ private static final int NEXT_SOURCE_STATE_INIT = 0;
+ private static final int NEXT_SOURCE_STATE_PREPARING = 1;
+ private static final int NEXT_SOURCE_STATE_PREPARED = 2;
+
/*
* Update the MediaPlayer2Impl SurfaceTexture.
* Call after setting a new display surface.
@@ -677,6 +660,21 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
}
}
+ @Override
+ public void notifyWhenCommandLabelReached(Object label) {
+ addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
+ @Override
+ void process() {
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onCommandLabelReached(
+ MediaPlayer2Impl.this, label));
+ }
+ }
+ }
+ });
+ }
+
/**
* Sets the {@link SurfaceHolder} to use for displaying the video
* portion of the media.
@@ -727,12 +725,17 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
*/
@Override
public void setSurface(Surface surface) {
- if (mScreenOnWhilePlaying && surface != null) {
- Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective for Surface");
- }
- mSurfaceHolder = null;
- _setVideoSurface(surface);
- updateSurfaceScreenOn();
+ addTask(new Task(CALL_COMPLETED_SET_SURFACE, false) {
+ @Override
+ void process() {
+ if (mScreenOnWhilePlaying && surface != null) {
+ Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective for Surface");
+ }
+ mSurfaceHolder = null;
+ _setVideoSurface(surface);
+ updateSurfaceScreenOn();
+ }
+ });
}
/**
@@ -756,20 +759,25 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
*/
@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();
- }
+ addTask(new Task(CALL_COMPLETED_SET_VIDEO_SCALING_MODE, false) {
+ @Override
+ void process() {
+ 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();
+ }
+ }
+ });
}
/**
@@ -779,314 +787,51 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
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.");
- }
+ private void addTask(Task task) {
+ synchronized (mTaskLock) {
+ mPendingTasks.add(task);
+ processPendingTask_l();
}
-
- 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) {
+ @GuardedBy("mTaskLock")
+ private void processPendingTask_l() {
+ if (mCurrentTask != null) {
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 (!mPendingTasks.isEmpty()) {
+ Task task = mPendingTasks.remove(0);
+ mCurrentTask = task;
+ mTaskHandler.post(task);
}
-
- 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 {
+ private void handleDataSource(boolean isCurrent, @NonNull DataSourceDesc dsd, long srcId)
+ throws IOException {
Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
switch (dsd.getType()) {
case DataSourceDesc.TYPE_CALLBACK:
- setDataSourcePriv(dsd.getId(),
- dsd.getMedia2DataSource());
+ handleDataSource(isCurrent,
+ srcId,
+ dsd.getMedia2DataSource());
break;
case DataSourceDesc.TYPE_FD:
- setDataSourcePriv(dsd.getId(),
- dsd.getFileDescriptor(),
- dsd.getFileDescriptorOffset(),
- dsd.getFileDescriptorLength());
+ handleDataSource(isCurrent,
+ srcId,
+ dsd.getFileDescriptor(),
+ dsd.getFileDescriptorOffset(),
+ dsd.getFileDescriptorLength());
break;
case DataSourceDesc.TYPE_URI:
- setDataSourcePriv(dsd.getId(),
- dsd.getUriContext(),
- dsd.getUri(),
- dsd.getUriHeaders(),
- dsd.getUriCookies());
+ handleDataSource(isCurrent,
+ srcId,
+ dsd.getUriContext(),
+ dsd.getUri(),
+ dsd.getUriHeaders(),
+ dsd.getUriCookies());
break;
default:
@@ -1113,66 +858,59 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @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,
+ private void handleDataSource(
+ boolean isCurrent, 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);
+ handleDataSource(isCurrent, srcId, uri.getPath(), null, null);
return;
- } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+ }
+
+ 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)) {
+ if (attemptDataSource(isCurrent, srcId, resolver, cacheUri)) {
return;
- } else if (attemptDataSource(srcId, resolver, actualUri)) {
+ }
+ if (attemptDataSource(isCurrent, srcId, resolver, actualUri)) {
return;
- } else {
- setDataSourcePriv(srcId, uri.toString(), headers, cookies);
}
+ handleDataSource(isCurrent, srcId, uri.toString(), headers, cookies);
} else {
// Try requested Uri locally first, or fallback to media server
- if (attemptDataSource(srcId, resolver, uri)) {
+ if (attemptDataSource(isCurrent, srcId, resolver, uri)) {
return;
- } else {
- setDataSourcePriv(srcId, uri.toString(), headers, cookies);
}
+ handleDataSource(isCurrent, srcId, uri.toString(), headers, cookies);
}
}
- private boolean attemptDataSource(long srcId, ContentResolver resolver, Uri uri) {
+ private boolean attemptDataSource(
+ boolean isCurrent, 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);
+ handleDataSource(isCurrent,
+ srcId,
+ afd.getFileDescriptor(),
+ 0,
+ DataSourceDesc.LONG_MAX);
} else {
- setDataSourcePriv(srcId,
- afd.getFileDescriptor(),
- afd.getStartOffset(),
- afd.getDeclaredLength());
+ handleDataSource(isCurrent,
+ srcId,
+ afd.getFileDescriptor(),
+ afd.getStartOffset(),
+ afd.getDeclaredLength());
}
return true;
} catch (NullPointerException | SecurityException | IOException ex) {
@@ -1181,10 +919,10 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
}
}
- private void setDataSourcePriv(
- long srcId, String path, Map<String, String> headers, List<HttpCookie> cookies)
- throws IOException, IllegalArgumentException, SecurityException, IllegalStateException
- {
+ private void handleDataSource(
+ boolean isCurrent, long srcId,
+ String path, Map<String, String> headers, List<HttpCookie> cookies)
+ throws IOException {
String[] keys = null;
String[] values = null;
@@ -1199,19 +937,21 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
++i;
}
}
- setDataSourcePriv(srcId, path, keys, values, cookies);
+ handleDataSource(isCurrent, srcId, path, keys, values, cookies);
}
- private void setDataSourcePriv(long srcId, String path, String[] keys, String[] values,
- List<HttpCookie> cookies)
- throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+ private void handleDataSource(boolean isCurrent, long srcId,
+ String path, String[] keys, String[] values, List<HttpCookie> cookies)
+ throws IOException {
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(
+ nativeHandleDataSourceUrl(
+ isCurrent,
+ srcId,
Media2HTTPService.createHTTPService(path, cookies),
path,
keys,
@@ -1223,16 +963,17 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
if (file.exists()) {
FileInputStream is = new FileInputStream(file);
FileDescriptor fd = is.getFD();
- setDataSourcePriv(srcId, fd, 0, DataSourceDesc.LONG_MAX);
+ handleDataSource(isCurrent, srcId, fd, 0, DataSourceDesc.LONG_MAX);
is.close();
} else {
- throw new IOException("setDataSourcePriv failed.");
+ throw new IOException("handleDataSource failed.");
}
}
- private native void nativeSetDataSource(
- Media2HTTPService httpService, String path, String[] keys, String[] values)
- throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;
+ private native void nativeHandleDataSourceUrl(
+ boolean isCurrent, long srcId,
+ Media2HTTPService httpService, String path, String[] keys, String[] values)
+ throws IOException;
/**
* Sets the data source (FileDescriptor) to use. The FileDescriptor must be
@@ -1243,76 +984,91 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @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(fd, offset, length);
+ private void handleDataSource(
+ boolean isCurrent, long srcId,
+ FileDescriptor fd, long offset, long length) throws IOException {
+ nativeHandleDataSourceFD(isCurrent, srcId, fd, offset, length);
}
- private native void _setDataSource(FileDescriptor fd, long offset, long length)
- throws IOException;
+ private native void nativeHandleDataSourceFD(boolean isCurrent, 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(dataSource);
+ private void handleDataSource(boolean isCurrent, long srcId, Media2DataSource dataSource) {
+ nativeHandleDataSourceCallback(isCurrent, srcId, dataSource);
}
- private native void _setDataSource(Media2DataSource dataSource);
+ private native void nativeHandleDataSourceCallback(
+ boolean isCurrent, 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();
+ // This function shall be called with |mSrcLock| acquired.
+ private void prepareNextDataSource_l() {
+ if (mNextDSDs == null || mNextDSDs.isEmpty()
+ || mNextSourceState != NEXT_SOURCE_STATE_INIT) {
+ // There is no next source or it's in preparing or prepared state.
+ return;
+ }
- // DrmInfo, if any, has been resolved by now.
- synchronized (mDrmLock) {
- mDrmInfoResolved = true;
+ try {
+ mNextSourceState = NEXT_SOURCE_STATE_PREPARING;
+ handleDataSource(false /* isCurrent */, mNextDSDs.get(0), mNextSrcId);
+ } catch (Exception e) {
+ Message msg2 = mEventHandler.obtainMessage(
+ MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
+ final long nextSrcId = mNextSrcId;
+ mEventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mEventHandler.handleMessage(msg2, nextSrcId);
+ }
+ });
}
}
- private native void _prepare() throws IOException, IllegalStateException;
+ // This function shall be called with |mSrcLock| acquired.
+ private void playNextDataSource_l() {
+ if (mNextDSDs == null || mNextDSDs.isEmpty()) {
+ return;
+ }
- /**
- * 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();
+ if (mNextSourceState == NEXT_SOURCE_STATE_PREPARED) {
+ // Switch to next source only when it's in prepared state.
+ mCurrentDSD = mNextDSDs.get(0);
+ mCurrentSrcId = mNextSrcId;
+ mBufferedPercentageCurrent.set(mBufferedPercentageNext.get());
+ mNextDSDs.remove(0);
+ mNextSrcId = mSrcIdGenerator++; // make it different from mCurrentSrcId
+ mBufferedPercentageNext.set(0);
+ mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ mNextSourcePlayPending = false;
- /**
- * 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();
+ long srcId = mCurrentSrcId;
+ try {
+ nativePlayNextDataSource(srcId);
+ } catch (Exception e) {
+ Message msg2 = mEventHandler.obtainMessage(
+ MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
+ mEventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mEventHandler.handleMessage(msg2, srcId);
+ }
+ });
+ }
+
+ // Wait for MEDIA2_INFO_STARTED_AS_NEXT to prepare next source.
+ } else {
+ if (mNextSourceState == NEXT_SOURCE_STATE_INIT) {
+ prepareNextDataSource_l();
+ }
+ mNextSourcePlayPending = true;
+ }
}
- private native void _start() throws IllegalStateException;
+ private native void nativePlayNextDataSource(long srcId);
private int getAudioStreamType() {
@@ -1339,20 +1095,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
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
//--------------------
@@ -1417,6 +1159,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
/*
* 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);
@@ -1562,9 +1305,10 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
*
* @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.
+ * yet. The {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+ * is available.
*/
@Override
public native int getVideoWidth();
@@ -1574,9 +1318,10 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
*
* @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.
+ * yet. The {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height
+ * is available.
*/
@Override
public native int getVideoHeight();
@@ -1605,10 +1350,18 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @return true if currently playing, false otherwise
* @throws IllegalStateException if the internal player engine has not been
* initialized or has been released.
+ * @hide
*/
@Override
public native boolean isPlaying();
+ @Override
+ public @MediaPlayer2State int getMediaPlayer2State() {
+ return native_getMediaPlayer2State();
+ }
+
+ private native int native_getMediaPlayer2State();
+
/**
* Gets the current buffering management params used by the source component.
* Calling it only after {@code setDataSource} has been called.
@@ -1638,7 +1391,17 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @hide
*/
@Override
- public native void setBufferingParams(@NonNull BufferingParams params);
+ public void setBufferingParams(@NonNull BufferingParams params) {
+ addTask(new Task(CALL_COMPLETED_SET_BUFFERING_PARAMS, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(params, "the BufferingParams cannot be null");
+ _setBufferingParams(params);
+ }
+ });
+ }
+
+ private native void _setBufferingParams(@NonNull BufferingParams params);
/**
* Sets playback rate and audio mode.
@@ -1692,7 +1455,17 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @throws IllegalArgumentException if params is not supported.
*/
@Override
- public native void setPlaybackParams(@NonNull PlaybackParams params);
+ public void setPlaybackParams(@NonNull PlaybackParams params) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(params, "the PlaybackParams cannot be null");
+ _setPlaybackParams(params);
+ }
+ });
+ }
+
+ private native void _setPlaybackParams(@NonNull PlaybackParams params);
/**
* Gets the playback params, containing the current playback rate.
@@ -1715,7 +1488,17 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @throws IllegalArgumentException if params are not supported.
*/
@Override
- public native void setSyncParams(@NonNull SyncParams params);
+ public void setSyncParams(@NonNull SyncParams params) {
+ addTask(new Task(CALL_COMPLETED_SET_SYNC_PARAMS, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(params, "the SyncParams cannot be null");
+ _setSyncParams(params);
+ }
+ });
+ }
+
+ private native void _setSyncParams(@NonNull SyncParams params);
/**
* Gets the A/V sync mode.
@@ -1729,8 +1512,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
@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>
@@ -1762,22 +1543,32 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @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);
+ public void seekTo(final long msec, @SeekMode int mode) {
+ addTask(new Task(CALL_COMPLETED_SEEK_TO, true) {
+ @Override
+ void process() {
+ 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.
+ long posMs = msec;
+ if (posMs > Integer.MAX_VALUE) {
+ Log.w(TAG, "seekTo offset " + posMs + " is too large, cap to "
+ + Integer.MAX_VALUE);
+ posMs = Integer.MAX_VALUE;
+ } else if (posMs < Integer.MIN_VALUE) {
+ Log.w(TAG, "seekTo offset " + posMs + " is too small, cap to "
+ + Integer.MIN_VALUE);
+ posMs = Integer.MIN_VALUE;
+ }
+ _seekTo(posMs, mode);
+ }
+ });
}
+ private native final void _seekTo(long msec, int mode);
+
/**
* Get current playback position as a {@link MediaTimestamp}.
* <p>
@@ -1812,23 +1603,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
}
/**
- * 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
@@ -1914,28 +1688,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
}
/**
- * 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().
@@ -1960,6 +1712,13 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
mTimeProvider = null;
}
+ synchronized (mEventCbLock) {
+ mEventCallbackRecords.clear();
+ }
+ synchronized (mDrmEventCbLock) {
+ mDrmEventCallbackRecords.clear();
+ }
+
stayAwake(false);
_reset();
// make sure none of the listeners get called anymore
@@ -1999,41 +1758,11 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @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();
- }
+ private native Parcel getParameter(int key);
- /**
- * 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.
@@ -2045,39 +1774,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
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.
@@ -2095,7 +1791,16 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @throws IllegalArgumentException if the sessionId is invalid.
*/
@Override
- public native void setAudioSessionId(int sessionId);
+ public void setAudioSessionId(int sessionId) {
+ addTask(new Task(CALL_COMPLETED_SET_AUDIO_SESSION_ID, false) {
+ @Override
+ void process() {
+ _setAudioSessionId(sessionId);
+ }
+ });
+ }
+
+ private native void _setAudioSessionId(int sessionId);
/**
* Returns the audio session ID.
@@ -2121,8 +1826,16 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @param effectId system wide unique id of the effect to attach
*/
@Override
- public native void attachAuxEffect(int effectId);
+ public void attachAuxEffect(int effectId) {
+ addTask(new Task(CALL_COMPLETED_ATTACH_AUX_EFFECT, false) {
+ @Override
+ void process() {
+ _attachAuxEffect(effectId);
+ }
+ });
+ }
+ private native void _attachAuxEffect(int effectId);
/**
* Sets the send level of the player to the attached auxiliary effect.
@@ -2138,7 +1851,12 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
*/
@Override
public void setAuxEffectSendLevel(float level) {
- _setAuxEffectSendLevel(level);
+ addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
+ @Override
+ void process() {
+ _setAuxEffectSendLevel(level);
+ }
+ });
}
private native void _setAuxEffectSendLevel(float level);
@@ -2181,6 +1899,13 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
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.
*
@@ -2870,7 +2595,12 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
*/
@Override
public void selectTrack(int index) {
- selectOrDeselectTrack(index, true /* select */);
+ addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
+ @Override
+ void process() {
+ selectOrDeselectTrack(index, true /* select */);
+ }
+ });
}
/**
@@ -2889,7 +2619,12 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
*/
@Override
public void deselectTrack(int index) {
- selectOrDeselectTrack(index, false /* select */);
+ addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
+ @Override
+ void process() {
+ selectOrDeselectTrack(index, false /* select */);
+ }
+ });
}
private void selectOrDeselectTrack(int index, boolean select)
@@ -2956,83 +2691,6 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
}
}
- /**
- * Sets the target UDP re-transmit endpoint for the low level player.
- * Generally, the address portion of the endpoint is an IP multicast
- * address, although a unicast address would be equally valid. When a valid
- * retransmit endpoint has been set, the media player will not decode and
- * render the media presentation locally. Instead, the player will attempt
- * to re-multiplex its media data using the Android@Home RTP profile and
- * re-transmit to the target endpoint. Receiver devices (which may be
- * either the same as the transmitting device or different devices) may
- * instantiate, prepare, and start a receiver player using a setDataSource
- * URL of the form...
- *
- * aahRX://&lt;multicastIP&gt;:&lt;port&gt;
- *
- * to receive, decode and render the re-transmitted content.
- *
- * setRetransmitEndpoint may only be called before setDataSource has been
- * called; while the player is in the Idle state.
- *
- * @param endpoint the address and UDP port of the re-transmission target or
- * null if no re-transmission is to be performed.
- * @throws IllegalStateException if it is called in an invalid state
- * @throws IllegalArgumentException if the retransmit endpoint is supplied,
- * but invalid.
- *
- * {@hide} pending API council
- */
- @Override
- public void setRetransmitEndpoint(InetSocketAddress endpoint)
- throws IllegalStateException, IllegalArgumentException
- {
- String addrString = null;
- int port = 0;
-
- if (null != endpoint) {
- addrString = endpoint.getAddress().getHostAddress();
- port = endpoint.getPort();
- }
-
- int ret = native_setRetransmitEndpoint(addrString, port);
- if (ret != 0) {
- throw new IllegalArgumentException("Illegal re-transmit endpoint; native ret " + ret);
- }
- }
-
- private native final int native_setRetransmitEndpoint(String addrString, int port);
-
- /**
- * 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
@@ -3049,8 +2707,11 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
stayAwake(false);
updateSurfaceScreenOn();
synchronized (mEventCbLock) {
- mEventCb = null;
- mEventExec = null;
+ mEventCallbackRecords.clear();
+ }
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread = null;
}
if (mTimeProvider != null) {
mTimeProvider.close();
@@ -3061,8 +2722,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
// Modular DRM clean up
mOnDrmConfigHelper = null;
synchronized (mDrmEventCbLock) {
- mDrmEventCb = null;
- mDrmEventExec = null;
+ mDrmEventCallbackRecords.clear();
}
resetDrmState();
@@ -3114,24 +2774,20 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
@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 Executor eventExec;
- final EventCallback eventCb;
- synchronized (mEventCbLock) {
- eventExec = mEventExec;
- eventCb = mEventCb;
- }
- final Executor drmEventExec;
- final DrmEventCallback drmEventCb;
- synchronized (mDrmEventCbLock) {
- drmEventExec = mDrmEventExec;
- drmEventCb = mDrmEventCb;
- }
+ final int what = msg.arg1;
+ final int extra = msg.arg2;
+
switch(msg.what) {
case MEDIA_PREPARED:
+ {
try {
scanInternalSubtitleTracks();
} catch (RuntimeException e) {
@@ -3143,174 +2799,273 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
sendMessage(msg2);
}
- if (eventCb != null && eventExec != null) {
- eventExec.execute(() -> eventCb.onInfo(
- mMediaPlayer, 0, MEDIA_INFO_PREPARED, 0));
+ final DataSourceDesc dsd;
+ synchronized (mSrcLock) {
+ Log.i(TAG, "MEDIA_PREPARED: srcId=" + srcId
+ + ", currentSrcId=" + mCurrentSrcId + ", nextSrcId=" + mNextSrcId);
+ if (srcId == mCurrentSrcId) {
+ dsd = mCurrentDSD;
+ prepareNextDataSource_l();
+ } else if (mNextDSDs != null && !mNextDSDs.isEmpty()
+ && srcId == mNextSrcId) {
+ dsd = mNextDSDs.get(0);
+ mNextSourceState = NEXT_SOURCE_STATE_PREPARED;
+ if (mNextSourcePlayPending) {
+ playNextDataSource_l();
+ }
+ } else {
+ dsd = null;
+ }
+ }
+
+ if (dsd != null) {
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onInfo(
+ mMediaPlayer, dsd, MEDIA_INFO_PREPARED, 0));
+ }
+ }
+ }
+ synchronized (mTaskLock) {
+ if (mCurrentTask != null
+ && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
+ && mCurrentTask.mDSD == dsd
+ && mCurrentTask.mNeedToWaitForEventToComplete) {
+ mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
}
return;
+ }
case MEDIA_DRM_INFO:
- Log.v(TAG, "MEDIA_DRM_INFO " + mDrmEventCb);
-
+ {
if (msg.obj == null) {
Log.w(TAG, "MEDIA_DRM_INFO msg.obj=NULL");
} else if (msg.obj instanceof Parcel) {
- if (drmEventExec != null && drmEventCb != null) {
- // The parcel was parsed already in postEventFromNative
- final DrmInfoImpl drmInfo;
-
- synchronized (mDrmLock) {
- if (mDrmInfoImpl != null) {
- drmInfo = mDrmInfoImpl.makeCopy();
- } else {
- drmInfo = null;
- }
+ // 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) {
- drmEventExec.execute(() -> drmEventCb.onDrmInfo(mMediaPlayer, drmInfo));
+ // notifying the client outside the lock
+ if (drmInfo != null) {
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onDrmInfo(
+ mMediaPlayer, mCurrentDSD, drmInfo));
+ }
}
}
} else {
Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + msg.obj);
}
return;
+ }
case MEDIA_PLAYBACK_COMPLETE:
- if (eventCb != null && eventExec != null) {
- eventExec.execute(() -> eventCb.onInfo(
- mMediaPlayer, 0, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+ {
+ final DataSourceDesc dsd = mCurrentDSD;
+ synchronized (mSrcLock) {
+ if (srcId == mCurrentSrcId) {
+ Log.i(TAG, "MEDIA_PLAYBACK_COMPLETE: srcId=" + srcId
+ + ", currentSrcId=" + mCurrentSrcId + ", nextSrcId=" + mNextSrcId);
+ playNextDataSource_l();
+ }
+ }
+
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onInfo(
+ mMediaPlayer, dsd, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+ }
}
stayAwake(false);
return;
+ }
case MEDIA_STOPPED:
- {
- TimeProvider timeProvider = mTimeProvider;
- if (timeProvider != null) {
- timeProvider.onStopped();
- }
+ {
+ 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);
- }
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onPaused(msg.what == MEDIA_PAUSED);
}
break;
+ }
case MEDIA_BUFFERING_UPDATE:
- if (eventCb != null && eventExec != null) {
- final int percent = msg.arg1;
- eventExec.execute(() -> eventCb.onBufferingUpdate(mMediaPlayer, 0, percent));
+ {
+ final int percent = msg.arg1;
+ synchronized (mEventCbLock) {
+ if (srcId == mCurrentSrcId) {
+ mBufferedPercentageCurrent.set(percent);
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onInfo(
+ mMediaPlayer, mCurrentDSD, MEDIA_INFO_BUFFERING_UPDATE,
+ percent));
+ }
+ } else if (srcId == mNextSrcId && !mNextDSDs.isEmpty()) {
+ mBufferedPercentageNext.set(percent);
+ DataSourceDesc nextDSD = mNextDSDs.get(0);
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onInfo(
+ mMediaPlayer, nextDSD, MEDIA_INFO_BUFFERING_UPDATE,
+ percent));
+ }
+ }
}
return;
+ }
case MEDIA_SEEK_COMPLETE:
- if (eventCb != null && eventExec != null) {
- eventExec.execute(() -> eventCb.onInfo(
- mMediaPlayer, 0, MEDIA_INFO_COMPLETE_CALL_SEEK, 0));
+ {
+ synchronized (mTaskLock) {
+ if (mCurrentTask != null
+ && mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
+ && mCurrentTask.mNeedToWaitForEventToComplete) {
+ mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
}
+ }
// fall through
case MEDIA_SKIPPED:
- {
- TimeProvider timeProvider = mTimeProvider;
- if (timeProvider != null) {
- timeProvider.onSeekComplete(mMediaPlayer);
- }
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onSeekComplete(mMediaPlayer);
}
return;
+ }
case MEDIA_SET_VIDEO_SIZE:
- if (eventCb != null && eventExec != null) {
- final int width = msg.arg1;
- final int height = msg.arg2;
- eventExec.execute(() -> eventCb.onVideoSizeChanged(
- mMediaPlayer, 0, width, height));
+ {
+ final int width = msg.arg1;
+ final int height = msg.arg2;
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onVideoSizeChanged(
+ mMediaPlayer, mCurrentDSD, width, height));
+ }
}
return;
+ }
case MEDIA_ERROR:
+ {
Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")");
- if (eventCb != null && eventExec != null) {
- final int what = msg.arg1;
- final int extra = msg.arg2;
- eventExec.execute(() -> eventCb.onError(mMediaPlayer, 0, what, extra));
- eventExec.execute(() -> eventCb.onInfo(
- mMediaPlayer, 0, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onError(
+ mMediaPlayer, mCurrentDSD, what, extra));
+ cb.first.execute(() -> cb.second.onInfo(
+ mMediaPlayer, mCurrentDSD, 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_STARTED_AS_NEXT:
+ if (srcId == mCurrentSrcId) {
+ prepareNextDataSource_l();
+ }
+ break;
- 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;
+ 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;
}
- if (eventCb != null && eventExec != null) {
- final int what = msg.arg1;
- final int extra = msg.arg2;
- eventExec.execute(() -> eventCb.onInfo(mMediaPlayer, 0, what, extra));
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onInfo(
+ mMediaPlayer, mCurrentDSD, what, extra));
+ }
}
// No real default action so far.
return;
+ }
case MEDIA_NOTIFY_TIME:
- TimeProvider timeProvider = mTimeProvider;
- if (timeProvider != null) {
- timeProvider.onNotifyTime();
- }
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onNotifyTime();
+ }
return;
+ }
case MEDIA_TIMED_TEXT:
- if (eventCb == null || eventExec == null) {
- return;
- }
- if (msg.obj == null) {
- eventExec.execute(() -> eventCb.onTimedText(mMediaPlayer, 0, null));
+ {
+ final TimedText text;
+ if (msg.obj instanceof Parcel) {
+ Parcel parcel = (Parcel)msg.obj;
+ text = new TimedText(parcel);
+ parcel.recycle();
} else {
- if (msg.obj instanceof Parcel) {
- Parcel parcel = (Parcel)msg.obj;
- TimedText text = new TimedText(parcel);
- parcel.recycle();
- eventExec.execute(() -> eventCb.onTimedText(mMediaPlayer, 0, text));
+ text = null;
+ }
+
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onTimedText(mMediaPlayer, mCurrentDSD, text));
}
}
return;
+ }
case MEDIA_SUBTITLE_DATA:
+ {
OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener;
if (onSubtitleDataListener == null) {
return;
@@ -3322,24 +3077,35 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
onSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
}
return;
+ }
case MEDIA_META_DATA:
- if (eventCb == null || eventExec == null) {
- return;
- }
+ {
+ final TimedMetaData data;
if (msg.obj instanceof Parcel) {
Parcel parcel = (Parcel) msg.obj;
- TimedMetaData data = TimedMetaData.createTimedMetaDataFromParcel(parcel);
+ data = TimedMetaData.createTimedMetaDataFromParcel(parcel);
parcel.recycle();
- eventExec.execute(() -> eventCb.onTimedMetaDataAvailable(
- mMediaPlayer, 0, data));
+ } else {
+ data = null;
+ }
+
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onTimedMetaDataAvailable(
+ mMediaPlayer, mCurrentDSD, data));
+ }
}
return;
+ }
case MEDIA_NOP: // interface test message - ignore
+ {
break;
+ }
case MEDIA_AUDIO_ROUTING_CHANGED:
+ {
AudioManager.resetAudioPortGeneration();
synchronized (mRoutingChangeListeners) {
for (NativeRoutingEventHandlerDelegate delegate
@@ -3348,11 +3114,14 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
}
}
return;
+ }
default:
+ {
Log.e(TAG, "Unknown message type " + msg.what);
return;
}
+ }
}
}
@@ -3363,7 +3132,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* 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,
+ 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();
@@ -3404,7 +3173,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
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
+ // mainly for prepare() 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) {
@@ -3416,13 +3185,19 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
if (mp.mEventHandler != null) {
Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj);
- mp.mEventHandler.sendMessage(m);
+
+ mp.mEventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mp.mEventHandler.handleMessage(m, srcId);
+ }
+ });
}
}
- private Executor mEventExec;
- private EventCallback mEventCb;
private final Object mEventCbLock = new Object();
+ private ArrayList<Pair<Executor, MediaPlayer2EventCallback> > mEventCallbackRecords
+ = new ArrayList<Pair<Executor, MediaPlayer2EventCallback> >();
/**
* Register a callback to be invoked when the media source is ready
@@ -3432,33 +3207,27 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @param executor the executor through which the callback should be invoked
*/
@Override
- public void registerEventCallback(@NonNull @CallbackExecutor Executor executor,
- @NonNull EventCallback eventCallback) {
+ public void setMediaPlayer2EventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull MediaPlayer2EventCallback eventCallback) {
if (eventCallback == null) {
- throw new IllegalArgumentException("Illegal null EventCallback");
+ throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
}
if (executor == null) {
- throw new IllegalArgumentException("Illegal null Executor for the EventCallback");
+ throw new IllegalArgumentException(
+ "Illegal null Executor for the MediaPlayer2EventCallback");
}
synchronized (mEventCbLock) {
- // TODO: support multiple callbacks.
- mEventExec = executor;
- mEventCb = eventCallback;
+ mEventCallbackRecords.add(new Pair(executor, eventCallback));
}
}
/**
- * Unregisters an {@link EventCallback}.
- *
- * @param callback an {@link EventCallback} to unregister
+ * Clears the {@link MediaPlayer2EventCallback}.
*/
@Override
- public void unregisterEventCallback(EventCallback callback) {
+ public void clearMediaPlayer2EventCallback() {
synchronized (mEventCbLock) {
- if (callback == mEventCb) {
- mEventExec = null;
- mEventCb = null;
- }
+ mEventCallbackRecords.clear();
}
}
@@ -3497,9 +3266,9 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
private OnDrmConfigHelper mOnDrmConfigHelper;
- private Executor mDrmEventExec;
- private DrmEventCallback mDrmEventCb;
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
@@ -3509,33 +3278,27 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* @param executor the executor through which the callback should be invoked
*/
@Override
- public void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
+ public void setDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull DrmEventCallback eventCallback) {
if (eventCallback == null) {
- throw new IllegalArgumentException("Illegal null EventCallback");
+ throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
}
if (executor == null) {
- throw new IllegalArgumentException("Illegal null Executor for the EventCallback");
+ throw new IllegalArgumentException(
+ "Illegal null Executor for the MediaPlayer2EventCallback");
}
synchronized (mDrmEventCbLock) {
- // TODO: support multiple callbacks.
- mDrmEventExec = executor;
- mDrmEventCb = eventCallback;
+ mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
}
}
/**
- * Unregisters a {@link DrmEventCallback}.
- *
- * @param callback a {@link DrmEventCallback} to unregister
+ * Clears the {@link DrmEventCallback}.
*/
@Override
- public void unregisterDrmEventCallback(DrmEventCallback callback) {
+ public void clearDrmEventCallback() {
synchronized (mDrmEventCbLock) {
- if (callback == mDrmEventCb) {
- mDrmEventExec = null;
- mDrmEventCb = null;
- }
+ mDrmEventCallbackRecords.clear();
}
}
@@ -3662,7 +3425,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
// call the callback outside the lock
if (mOnDrmConfigHelper != null) {
- mOnDrmConfigHelper.onDrmConfig(this);
+ mOnDrmConfigHelper.onDrmConfig(this, mCurrentDSD);
}
synchronized (mDrmLock) {
@@ -3733,15 +3496,11 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
// if finished successfully without provisioning, call the callback outside the lock
if (allDoneWithoutProvisioning) {
- final Executor drmEventExec;
- final DrmEventCallback drmEventCb;
synchronized (mDrmEventCbLock) {
- drmEventExec = mDrmEventExec;
- drmEventCb = mDrmEventCb;
- }
- if (drmEventExec != null && drmEventCb != null) {
- drmEventExec.execute(() -> drmEventCb.onDrmPrepared(
- this, PREPARE_DRM_STATUS_SUCCESS));
+ for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onDrmPrepared(
+ this, mCurrentDSD, PREPARE_DRM_STATUS_SUCCESS));
+ }
}
}
@@ -3763,32 +3522,39 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
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();
+ addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
+ @Override
+ void process() throws NoDrmSchemeException {
+ synchronized (mDrmLock) {
+ Log.v(TAG, "releaseDrm:");
+
+ if (!mActiveDrmScheme) {
+ Log.e(TAG, "releaseDrm(): No active DRM scheme to release.");
+ throw new NoDrmSchemeExceptionImpl(
+ "releaseDrm: No active DRM scheme to release.");
+ }
- 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);
+ 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
}
- } // synchronized
+ });
}
@@ -3796,14 +3562,14 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* 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
+ * getDrmKeyRequest() 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}.
+ * {@link #provideDrmKeyResponse}.
*
* @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
@@ -3831,19 +3597,20 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
*/
@Override
@NonNull
- public MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
+ public MediaDrm.KeyRequest getDrmKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
@Nullable String mimeType, @MediaDrm.KeyType int keyType,
@Nullable Map<String, String> optionalParameters)
throws NoDrmSchemeException
{
- Log.v(TAG, "getKeyRequest: " +
+ Log.v(TAG, "getDrmKeyRequest: " +
" 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.");
+ Log.e(TAG, "getDrmKeyRequest NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl(
+ "getDrmKeyRequest: Has to set a DRM scheme first.");
}
try {
@@ -3858,16 +3625,16 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
MediaDrm.KeyRequest request = mDrmObj.getKeyRequest(scope, initData, mimeType,
keyType, hmapOptionalParameters);
- Log.v(TAG, "getKeyRequest: --> request: " + request);
+ Log.v(TAG, "getDrmKeyRequest: --> request: " + request);
return request;
} catch (NotProvisionedException e) {
- Log.w(TAG, "getKeyRequest NotProvisionedException: " +
+ Log.w(TAG, "getDrmKeyRequest NotProvisionedException: " +
"Unexpected. Shouldn't have reached here.");
- throw new IllegalStateException("getKeyRequest: Unexpected provisioning error.");
+ throw new IllegalStateException("getDrmKeyRequest: Unexpected provisioning error.");
} catch (Exception e) {
- Log.w(TAG, "getKeyRequest Exception " + e);
+ Log.w(TAG, "getDrmKeyRequest Exception " + e);
throw e;
}
@@ -3877,15 +3644,15 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
/**
* 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 DRM engine plugin using provideDrmKeyResponse. 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}.
+ * {@ link # restoreDrmKeys}.
* 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
+ * passed to the earlier {@ link #getDrmKeyRequest} 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
@@ -3895,16 +3662,17 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
* server rejected the request
*/
@Override
- public byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+ public byte[] provideDrmKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
throws NoDrmSchemeException, DeniedByServerException
{
- Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response);
+ Log.v(TAG, "provideDrmKeyResponse: keySetId: " + keySetId + " response: " + response);
synchronized (mDrmLock) {
if (!mActiveDrmScheme) {
- Log.e(TAG, "getKeyRequest NoDrmSchemeException");
- throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first.");
+ Log.e(TAG, "getDrmKeyRequest NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl(
+ "getDrmKeyRequest: Has to set a DRM scheme first.");
}
try {
@@ -3914,19 +3682,19 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
byte[] keySetResult = mDrmObj.provideKeyResponse(scope, response);
- Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response +
- " --> " + keySetResult);
+ Log.v(TAG, "provideDrmKeyResponse: keySetId: " + keySetId + " response: " + response
+ + " --> " + keySetResult);
return keySetResult;
} catch (NotProvisionedException e) {
- Log.w(TAG, "provideKeyResponse NotProvisionedException: " +
+ Log.w(TAG, "provideDrmKeyResponse NotProvisionedException: " +
"Unexpected. Shouldn't have reached here.");
- throw new IllegalStateException("provideKeyResponse: " +
+ throw new IllegalStateException("provideDrmKeyResponse: " +
"Unexpected provisioning error.");
} catch (Exception e) {
- Log.w(TAG, "provideKeyResponse Exception " + e);
+ Log.w(TAG, "provideDrmKeyResponse Exception " + e);
throw e;
}
} // synchronized
@@ -3935,31 +3703,37 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
/**
* Restore persisted offline keys into a new session. keySetId identifies the
- * keys to load, obtained from a prior call to {@link #provideKeyResponse}.
+ * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
*
* @param keySetId identifies the saved key set to restore
*/
@Override
- public void restoreKeys(@NonNull byte[] keySetId)
+ public void restoreDrmKeys(@NonNull byte[] keySetId)
throws NoDrmSchemeException
{
- Log.v(TAG, "restoreKeys: keySetId: " + keySetId);
+ addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
+ @Override
+ void process() throws NoDrmSchemeException {
+ Log.v(TAG, "restoreDrmKeys: keySetId: " + keySetId);
- synchronized (mDrmLock) {
+ synchronized (mDrmLock) {
- if (!mActiveDrmScheme) {
- Log.w(TAG, "restoreKeys NoDrmSchemeException");
- throw new NoDrmSchemeExceptionImpl("restoreKeys: Has to set a DRM scheme first.");
- }
+ if (!mActiveDrmScheme) {
+ Log.w(TAG, "restoreDrmKeys NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl(
+ "restoreDrmKeys: Has to set a DRM scheme first.");
+ }
- try {
- mDrmObj.restoreKeys(mDrmSessionId, keySetId);
- } catch (Exception e) {
- Log.w(TAG, "restoreKeys Exception " + e);
- throw e;
- }
+ try {
+ mDrmObj.restoreKeys(mDrmSessionId, keySetId);
+ } catch (Exception e) {
+ Log.w(TAG, "restoreKeys Exception " + e);
+ throw e;
+ }
- } // synchronized
+ } // synchronized
+ }
+ });
}
@@ -3984,7 +3758,8 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
if (!mActiveDrmScheme && !mDrmConfigAllowed) {
Log.w(TAG, "getDrmPropertyString NoDrmSchemeException");
- throw new NoDrmSchemeExceptionImpl("getDrmPropertyString: Has to prepareDrm() first.");
+ throw new NoDrmSchemeExceptionImpl(
+ "getDrmPropertyString: Has to prepareDrm() first.");
}
try {
@@ -4022,7 +3797,8 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
if ( !mActiveDrmScheme && !mDrmConfigAllowed ) {
Log.w(TAG, "setDrmPropertyString NoDrmSchemeException");
- throw new NoDrmSchemeExceptionImpl("setDrmPropertyString: Has to prepareDrm() first.");
+ throw new NoDrmSchemeExceptionImpl(
+ "setDrmPropertyString: Has to prepareDrm() first.");
}
try {
@@ -4233,7 +4009,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
// 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
+ // at prepareDrm/openSession rather than getDrmKeyRequest/provideDrmKeyResponse
try {
mDrmSessionId = mDrmObj.openSession();
Log.v(TAG, "prepareDrm_openSessionStep: mDrmSessionId=" + mDrmSessionId);
@@ -4250,6 +4026,65 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
}
+ // 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;
@@ -4324,14 +4159,12 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
boolean succeeded = false;
- final Executor drmEventExec;
- final DrmEventCallback drmEventCb;
+ boolean hasCallback = false;
synchronized (mDrmEventCbLock) {
- drmEventExec = mDrmEventExec;
- drmEventCb = mDrmEventCb;
+ hasCallback = !mDrmEventCallbackRecords.isEmpty();
}
// non-blocking mode needs the lock
- if (drmEventExec != null && drmEventCb != null) {
+ if (hasCallback) {
synchronized (drmLock) {
// continuing with prepareDrm
@@ -4349,7 +4182,12 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
} // synchronized
// calling the callback outside the lock
- drmEventExec.execute(() -> drmEventCb.onDrmPrepared(mediaPlayer, status));
+ synchronized (mDrmEventCbLock) {
+ for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onDrmPrepared(
+ mediaPlayer, mCurrentDSD, status));
+ }
+ }
} else { // blocking mode already has the lock
// continuing with prepareDrm
@@ -4397,13 +4235,11 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
int result;
// non-blocking: this is not the final result
- final Executor drmEventExec;
- final DrmEventCallback drmEventCb;
+ boolean hasCallback = false;
synchronized (mDrmEventCbLock) {
- drmEventExec = mDrmEventExec;
- drmEventCb = mDrmEventCb;
+ hasCallback = !mDrmEventCallbackRecords.isEmpty();
}
- if (drmEventCb != null && drmEventExec != null) {
+ if (hasCallback) {
result = PREPARE_DRM_STATUS_SUCCESS;
} else {
// if blocking mode, wait till provisioning is done
@@ -4523,7 +4359,7 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
// no need for log(N) search performance
private MediaTimeProvider.OnMediaTimeListener mListeners[];
private long mTimes[];
- private Handler mEventHandler;
+ private EventHandler mEventHandler;
private boolean mRefresh = false;
private boolean mPausing = false;
private boolean mSeeking = false;
@@ -4896,4 +4732,65 @@ public final class MediaPlayer2Impl extends MediaPlayer2 {
}
}
}
+
+ private abstract class Task implements Runnable {
+ private final int mMediaCallType;
+ private final boolean mNeedToWaitForEventToComplete;
+ private DataSourceDesc mDSD;
+
+ public Task (int mediaCallType, boolean needToWaitForEventToComplete) {
+ mMediaCallType = mediaCallType;
+ mNeedToWaitForEventToComplete = needToWaitForEventToComplete;
+ }
+
+ abstract void process() throws IOException, NoDrmSchemeException;
+
+ @Override
+ public void run() {
+ int status = CALL_STATUS_NO_ERROR;
+ try {
+ process();
+ } catch (IllegalStateException e) {
+ status = CALL_STATUS_INVALID_OPERATION;
+ } catch (IllegalArgumentException e) {
+ status = CALL_STATUS_BAD_VALUE;
+ } catch (SecurityException e) {
+ status = CALL_STATUS_PERMISSION_DENIED;
+ } catch (IOException e) {
+ status = CALL_STATUS_ERROR_IO;
+ } catch (NoDrmSchemeException e) {
+ status = CALL_STATUS_NO_DRM_SCHEME;
+ } catch (Exception e) {
+ status = CALL_STATUS_ERROR_UNKNOWN;
+ }
+ synchronized (mSrcLock) {
+ mDSD = mCurrentDSD;
+ }
+
+ // TODO: Make native implementations asynchronous and let them send notifications.
+ if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
+
+ sendCompleteNotification(status);
+
+ synchronized (mTaskLock) {
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
+ }
+ }
+
+ private void sendCompleteNotification(int status) {
+ // In {@link #notifyWhenCommandLabelReached} case, a separate callback
+ // {#link #onCommandLabelReached} is already called in {@code process()}.
+ if (mMediaCallType == CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED) {
+ return;
+ }
+ synchronized (mEventCbLock) {
+ for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+ cb.first.execute(() -> cb.second.onCallCompleted(
+ MediaPlayer2Impl.this, mDSD, mMediaCallType, status));
+ }
+ }
+ }
+ };
}
diff --git a/android/media/MediaPlayerBase.java b/android/media/MediaPlayerBase.java
index d638a9f9..a4265525 100644
--- a/android/media/MediaPlayerBase.java
+++ b/android/media/MediaPlayerBase.java
@@ -16,57 +16,316 @@
package android.media;
-import android.media.MediaSession2.PlaylistParam;
-import android.media.session.PlaybackState;
-import android.os.Handler;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.concurrent.Executor;
/**
- * Base interfaces for all media players that want media session.
- *
* @hide
+ * Base class for all media players that want media session.
*/
-public abstract class MediaPlayerBase {
+public abstract class MediaPlayerBase implements AutoCloseable {
/**
- * Listens change in {@link PlaybackState2}.
+ * @hide
*/
- public interface PlaybackListener {
- /**
- * Called when {@link PlaybackState2} for this player is changed.
- */
- void onPlaybackChanged(PlaybackState2 state);
- }
+ @IntDef({
+ PLAYER_STATE_IDLE,
+ PLAYER_STATE_PAUSED,
+ PLAYER_STATE_PLAYING,
+ PLAYER_STATE_ERROR })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PlayerState {}
+
+ /**
+ * @hide
+ */
+ @IntDef({
+ BUFFERING_STATE_UNKNOWN,
+ BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+ BUFFERING_STATE_BUFFERING_AND_STARVED,
+ BUFFERING_STATE_BUFFERING_COMPLETE })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BuffState {}
+
+ /**
+ * State when the player is idle, and needs configuration to start playback.
+ */
+ public static final int PLAYER_STATE_IDLE = 0;
+
+ /**
+ * State when the player's playback is paused
+ */
+ public static final int PLAYER_STATE_PAUSED = 1;
+
+ /**
+ * State when the player's playback is ongoing
+ */
+ public static final int PLAYER_STATE_PLAYING = 2;
+
+ /**
+ * State when the player is in error state and cannot be recovered self.
+ */
+ public static final int PLAYER_STATE_ERROR = 3;
+
+ /**
+ * Buffering state is unknown.
+ */
+ public static final int BUFFERING_STATE_UNKNOWN = 0;
+
+ /**
+ * Buffering state indicating the player is buffering but enough has been buffered
+ * for this player to be able to play the content.
+ * See {@link #getBufferedPosition()} for how far is buffered already.
+ */
+ public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1;
+
+ /**
+ * Buffering state indicating the player is buffering, but the player is currently starved
+ * for data, and cannot play.
+ */
+ public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2;
+
+ /**
+ * Buffering state indicating the player is done buffering, and the remainder of the content is
+ * available for playback.
+ */
+ public static final int BUFFERING_STATE_BUFFERING_COMPLETE = 3;
+ /**
+ * Starts or resumes playback.
+ */
public abstract void play();
+
+ /**
+ * Prepares the player for playback.
+ * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerBase, DataSourceDesc)} for being
+ * notified when the preparation phase completed. During this time, the player may allocate
+ * resources required to play, such as audio and video decoders.
+ */
public abstract void prepare();
+
+ /**
+ * Pauses playback.
+ */
public abstract void pause();
- public abstract void stop();
- public abstract void skipToPrevious();
+
+ /**
+ * Resets the MediaPlayerBase to its uninitialized state.
+ */
+ public abstract void reset();
+
+ /**
+ *
+ */
public abstract void skipToNext();
+
+ /**
+ * Moves the playback head to the specified position
+ * @param pos the new playback position expressed in ms.
+ */
public abstract void seekTo(long pos);
- public abstract void fastFoward();
- public abstract void rewind();
- public abstract PlaybackState2 getPlaybackState();
- public abstract AudioAttributes getAudioAttributes();
+ public static final long UNKNOWN_TIME = -1;
+
+ /**
+ * Gets the current playback head position.
+ * @return the current playback position in ms, or {@link #UNKNOWN_TIME} if unknown.
+ */
+ public long getCurrentPosition() { return UNKNOWN_TIME; }
- public abstract void setPlaylist(List<MediaItem2> item, PlaylistParam param);
- public abstract void setCurrentPlaylistItem(int index);
+ /**
+ * Returns the duration of the current data source, or {@link #UNKNOWN_TIME} if unknown.
+ * @return the duration in ms, or {@link #UNKNOWN_TIME}.
+ */
+ public long getDuration() { return UNKNOWN_TIME; }
/**
- * 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
+ * Gets the buffered position of current playback, or {@link #UNKNOWN_TIME} if unknown.
+ * @return the buffered position in ms, or {@link #UNKNOWN_TIME}.
*/
- public abstract void addPlaybackListener(Executor executor, PlaybackListener listener);
+ public long getBufferedPosition() { return UNKNOWN_TIME; }
/**
- * Remove previously added {@link PlaybackListener}.
+ * Returns the current player state.
+ * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerBase, int)} for
+ * notification of changes.
+ * @return the current player state
+ */
+ public abstract @PlayerState int getPlayerState();
+
+ /**
+ * Returns the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ * @return the buffering state.
+ */
+ public abstract @BuffState int getBufferingState();
+
+ /**
+ * Sets the {@link AudioAttributes} to be used during the playback of the media.
*
- * @param listener the listener to be removed
+ * @param attributes non-null <code>AudioAttributes</code>.
+ */
+ public abstract void setAudioAttributes(@NonNull AudioAttributes attributes);
+
+ /**
+ * Returns AudioAttributes that media player has.
+ */
+ public abstract @Nullable AudioAttributes getAudioAttributes();
+
+ /**
+ * Sets the data source to be played.
+ * @param dsd
+ */
+ public abstract void setDataSource(@NonNull DataSourceDesc dsd);
+
+ /**
+ * Sets the data source that will be played immediately after the current one is done playing.
+ * @param dsd
+ */
+ public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
+
+ /**
+ * Sets the list of data sources that will be sequentially played after the current one. Each
+ * data source is played immediately after the previous one is done playing.
+ * @param dsds
+ */
+ public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
+
+ /**
+ * Returns the current data source.
+ * @return the current data source, or null if none is set, or none available to play.
+ */
+ public abstract @Nullable DataSourceDesc getCurrentDataSource();
+
+ /**
+ * Configures the player to loop on the current data source.
+ * @param loop true if the current data source is meant to loop.
+ */
+ public abstract void loopCurrent(boolean loop);
+
+ /**
+ * Sets the playback speed.
+ * A value of 1.0f is the default playback value.
+ * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+ * before using negative values.<br>
+ * After changing the playback speed, it is recommended to query the actual speed supported
+ * by the player, see {@link #getPlaybackSpeed()}.
+ * @param speed
+ */
+ public abstract void setPlaybackSpeed(float speed);
+
+ /**
+ * Returns the actual playback speed to be used by the player when playing.
+ * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+ * @return the actual playback speed
+ */
+ public float getPlaybackSpeed() { return 1.0f; }
+
+ /**
+ * Indicates whether reverse playback is supported.
+ * Reverse playback is indicated by negative playback speeds, see
+ * {@link #setPlaybackSpeed(float)}.
+ * @return true if reverse playback is supported.
+ */
+ public boolean isReversePlaybackSupported() { return false; }
+
+ /**
+ * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+ * on the audio samples.
+ * Note that this volume is specific to the player, and is separate from stream volume
+ * used across the platform.<br>
+ * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+ * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+ * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+ */
+ public abstract void setPlayerVolume(float volume);
+
+ /**
+ * Returns the current volume of this player to this player.
+ * Note that it does not take into account the associated stream volume.
+ * @return the player volume.
+ */
+ public abstract float getPlayerVolume();
+
+ /**
+ * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+ */
+ public float getMaxPlayerVolume() { return 1.0f; }
+
+ /**
+ * Adds a callback to be notified of events for this player.
+ * @param e the {@link Executor} to be used for the events.
+ * @param cb the callback to receive the events.
+ */
+ public abstract void registerPlayerEventCallback(@NonNull Executor e,
+ @NonNull PlayerEventCallback cb);
+
+ /**
+ * Removes a previously registered callback for player events
+ * @param cb the callback to remove
+ */
+ public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
+
+ /**
+ * A callback class to receive notifications for events on the media player.
+ * See {@link MediaPlayerBase#registerPlayerEventCallback(Executor, PlayerEventCallback)} to
+ * register this callback.
*/
- public abstract void removePlaybackListener(PlaybackListener listener);
+ public static abstract class PlayerEventCallback {
+ /**
+ * Called when the player's current data source has changed.
+ *
+ * @param mpb the player whose data source changed.
+ * @param dsd the new current data source. null, if no more data sources available.
+ */
+ public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+ @Nullable DataSourceDesc dsd) { }
+ /**
+ * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
+ * referenced by the given data source.
+ * @param mpb the player that is prepared.
+ * @param dsd the data source that the player is prepared to play.
+ */
+ public void onMediaPrepared(@NonNull MediaPlayerBase mpb, @NonNull DataSourceDesc dsd) { }
+
+ /**
+ * Called to indicate that the state of the player has changed.
+ * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+ * @param mpb the player whose state has changed.
+ * @param state the new state of the player.
+ */
+ public void onPlayerStateChanged(@NonNull MediaPlayerBase mpb, @PlayerState int state) { }
+
+ /**
+ * Called to report buffering events for a data source.
+ * @param mpb the player that is buffering
+ * @param dsd the data source for which buffering is happening.
+ * @param state the new buffering state.
+ */
+ public void onBufferingStateChanged(@NonNull MediaPlayerBase mpb,
+ @NonNull DataSourceDesc dsd, @BuffState int state) { }
+
+ /**
+ * Called to indicate that the playback speed has changed.
+ * @param mpb the player that has changed the playback speed.
+ * @param speed the new playback speed.
+ */
+ public void onPlaybackSpeedChanged(@NonNull MediaPlayerBase mpb, float speed) { }
+
+ /**
+ * Called to indicate that {@link #seekTo(long)} is completed.
+ *
+ * @param mpb the player that has completed seeking.
+ * @param position the previous seeking request.
+ * @see #seekTo(long)
+ */
+ public void onSeekCompleted(@NonNull MediaPlayerBase mpb, long position) { }
+ }
+
}
diff --git a/android/media/MediaPlaylistAgent.java b/android/media/MediaPlaylistAgent.java
new file mode 100644
index 00000000..88f37e72
--- /dev/null
+++ b/android/media/MediaPlaylistAgent.java
@@ -0,0 +1,357 @@
+/*
+ * 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.media.update.ApiLoader;
+import android.media.update.MediaPlaylistAgentProvider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ * MediaPlaylistAgent is the abstract class an application needs to derive from to pass an object
+ * to a MediaSession2 that will override default playlist handling behaviors. It contains a set of
+ * notify methods to signal MediaSession2 that playlist-related state has changed.
+ * <p>
+ * Playlists are composed of one or multiple {@link MediaItem2} instances, which combine metadata
+ * and data sources (as {@link DataSourceDesc})
+ * Used by {@link MediaSession2} and {@link MediaController2}.
+ */
+// This class only includes methods that contain {@link MediaItem2}.
+public abstract class MediaPlaylistAgent {
+ /**
+ * @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. An example of a group is the playlist.
+ */
+ 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. An example of a group is the playlist.
+ */
+ public static final int SHUFFLE_MODE_GROUP = 2;
+
+ private final MediaPlaylistAgentProvider mProvider;
+
+ /**
+ * A callback class to receive notifications for events on the media player. See
+ * {@link MediaPlaylistAgent#registerPlaylistEventCallback(Executor, PlaylistEventCallback)}
+ * to register this callback.
+ */
+ public static abstract class PlaylistEventCallback {
+ /**
+ * Called when a playlist is changed.
+ *
+ * @param playlistAgent playlist agent for this event
+ * @param list new playlist
+ * @param metadata new metadata
+ */
+ public void onPlaylistChanged(@NonNull MediaPlaylistAgent playlistAgent,
+ @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when a playlist metadata is changed.
+ *
+ * @param playlistAgent playlist agent for this event
+ * @param metadata new metadata
+ */
+ public void onPlaylistMetadataChanged(@NonNull MediaPlaylistAgent playlistAgent,
+ @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when the shuffle mode is changed.
+ *
+ * @param playlistAgent playlist agent for this event
+ * @param shuffleMode repeat mode
+ * @see #SHUFFLE_MODE_NONE
+ * @see #SHUFFLE_MODE_ALL
+ * @see #SHUFFLE_MODE_GROUP
+ */
+ public void onShuffleModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
+ @ShuffleMode int shuffleMode) { }
+
+ /**
+ * Called when the repeat mode is changed.
+ *
+ * @param playlistAgent playlist agent for this event
+ * @param repeatMode repeat mode
+ * @see #REPEAT_MODE_NONE
+ * @see #REPEAT_MODE_ONE
+ * @see #REPEAT_MODE_ALL
+ * @see #REPEAT_MODE_GROUP
+ */
+ public void onRepeatModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
+ @RepeatMode int repeatMode) { }
+ }
+
+ public MediaPlaylistAgent() {
+ mProvider = ApiLoader.getProvider().createMediaPlaylistAgent(this);
+ }
+
+ /**
+ * Register {@link PlaylistEventCallback} to listen changes in the underlying
+ * {@link MediaPlaylistAgent}.
+ *
+ * @param executor a callback Executor
+ * @param callback a PlaylistEventCallback
+ * @throws IllegalArgumentException if executor or callback is {@code null}.
+ */
+ public final void registerPlaylistEventCallback(
+ @NonNull @CallbackExecutor Executor executor, @NonNull PlaylistEventCallback callback) {
+ mProvider.registerPlaylistEventCallback_impl(executor, callback);
+ }
+
+ /**
+ * Unregister the previously registered {@link PlaylistEventCallback}.
+ *
+ * @param callback the callback to be removed
+ * @throws IllegalArgumentException if the callback is {@code null}.
+ */
+ public final void unregisterPlaylistEventCallback(@NonNull PlaylistEventCallback callback) {
+ mProvider.unregisterPlaylistEventCallback_impl(callback);
+ }
+
+ public final void notifyPlaylistChanged() {
+ mProvider.notifyPlaylistChanged_impl();
+ }
+
+ public final void notifyPlaylistMetadataChanged() {
+ mProvider.notifyPlaylistMetadataChanged_impl();
+ }
+
+ public final void notifyShuffleModeChanged() {
+ mProvider.notifyShuffleModeChanged_impl();
+ }
+
+ public final void notifyRepeatModeChanged() {
+ mProvider.notifyRepeatModeChanged_impl();
+ }
+
+ /**
+ * Returns the playlist
+ *
+ * @return playlist, or null if none is set.
+ */
+ public @Nullable List<MediaItem2> getPlaylist() {
+ return mProvider.getPlaylist_impl();
+ }
+
+ /**
+ * Sets the playlist.
+ *
+ * @param list playlist
+ * @param metadata metadata of the playlist
+ */
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ mProvider.setPlaylist_impl(list, metadata);
+ }
+
+ /**
+ * Returns the playlist metadata
+ *
+ * @return metadata metadata of the playlist, or null if none is set
+ */
+ public @Nullable MediaMetadata2 getPlaylistMetadata() {
+ return mProvider.getPlaylistMetadata_impl();
+ }
+
+ /**
+ * Updates the playlist metadata
+ *
+ * @param metadata metadata of the playlist
+ */
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ mProvider.updatePlaylistMetadata_impl(metadata);
+ }
+
+ /**
+ * Adds the media item to the playlist at position index. Index equals or greater than
+ * the current playlist size will add the item at the end of the playlist.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the playlist,
+ * the current index of the playlist will be incremented correspondingly.
+ *
+ * @param index the index you want to add
+ * @param item the media item you want to add
+ */
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ mProvider.addPlaylistItem_impl(index, item);
+ }
+
+ /**
+ * Removes the media item from the playlist
+ *
+ * @param item media item to remove
+ */
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ mProvider.removePlaylistItem_impl(item);
+ }
+
+ /**
+ * Replace the media item at index in the playlist. This can be also used to update metadata of
+ * an item.
+ *
+ * @param index the index of the item to replace
+ * @param item the new item
+ */
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ mProvider.replacePlaylistItem_impl(index, item);
+ }
+
+ /**
+ * Skips to the the media item, and plays from it.
+ *
+ * @param item media item to start playing from
+ */
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ mProvider.skipToPlaylistItem_impl(item);
+ }
+
+ /**
+ * Skips to the previous item in the playlist.
+ */
+ public void skipToPreviousItem() {
+ mProvider.skipToPreviousItem_impl();
+ }
+
+ /**
+ * Skips to the next item in the playlist.
+ */
+ public void skipToNextItem() {
+ mProvider.skipToNextItem_impl();
+ }
+
+ /**
+ * Gets the repeat mode
+ *
+ * @return repeat mode
+ * @see #REPEAT_MODE_NONE
+ * @see #REPEAT_MODE_ONE
+ * @see #REPEAT_MODE_ALL
+ * @see #REPEAT_MODE_GROUP
+ */
+ public @RepeatMode int getRepeatMode() {
+ return mProvider.getRepeatMode_impl();
+ }
+
+ /**
+ * Sets the repeat mode
+ *
+ * @param repeatMode repeat mode
+ * @see #REPEAT_MODE_NONE
+ * @see #REPEAT_MODE_ONE
+ * @see #REPEAT_MODE_ALL
+ * @see #REPEAT_MODE_GROUP
+ */
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ mProvider.setRepeatMode_impl(repeatMode);
+ }
+
+ /**
+ * Gets the shuffle mode
+ *
+ * @return The shuffle mode
+ * @see #SHUFFLE_MODE_NONE
+ * @see #SHUFFLE_MODE_ALL
+ * @see #SHUFFLE_MODE_GROUP
+ */
+ public @ShuffleMode int getShuffleMode() {
+ return mProvider.getShuffleMode_impl();
+ }
+
+ /**
+ * Sets the shuffle mode
+ *
+ * @param shuffleMode The shuffle mode
+ * @see #SHUFFLE_MODE_NONE
+ * @see #SHUFFLE_MODE_ALL
+ * @see #SHUFFLE_MODE_GROUP
+ */
+ public void setShuffleMode(@ShuffleMode int shuffleMode) {
+ mProvider.setShuffleMode_impl(shuffleMode);
+ }
+
+ /**
+ * Called by {@link MediaSession2} when it wants to translate {@link DataSourceDesc} from the
+ * {@link MediaPlayerBase.PlayerEventCallback} to the {@link MediaItem2}. Override this method
+ * if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them with
+ * {@link #setPlaylist(List, MediaMetadata2)}.
+ * <p>
+ * Session would throw an exception if this returns {@code null} for {@param dsd} from the
+ * {@link MediaPlayerBase.PlayerEventCallback}.
+ * <p>
+ * Default implementation calls the {@link #getPlaylist()} and searches the {@link MediaItem2}
+ * with the {@param dsd}.
+ *
+ * @param dsd The dsd to query.
+ * @return A {@link MediaItem2} object in the playlist that matches given {@code dsd}.
+ * @throws IllegalArgumentException if {@code dsd} is null
+ */
+ public @Nullable MediaItem2 getMediaItem(@NonNull DataSourceDesc dsd) {
+ return mProvider.getMediaItem_impl(dsd);
+ }
+}
diff --git a/android/media/MediaRecorder.java b/android/media/MediaRecorder.java
index 78477f75..90b6bff6 100644
--- a/android/media/MediaRecorder.java
+++ b/android/media/MediaRecorder.java
@@ -17,6 +17,7 @@
package android.media;
import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.ActivityThread;
import android.hardware.Camera;
@@ -25,8 +26,10 @@ 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.util.Pair;
import android.view.Surface;
import java.io.File;
@@ -34,6 +37,8 @@ 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;
@@ -101,6 +106,8 @@ public class MediaRecorder implements AudioRouting
private OnErrorListener mOnErrorListener;
private OnInfoListener mOnInfoListener;
+ private int mChannelCount;
+
/**
* Default constructor.
*/
@@ -115,6 +122,7 @@ public class MediaRecorder implements AudioRouting
mEventHandler = null;
}
+ mChannelCount = 1;
String packageName = ActivityThread.currentPackageName();
/* Native setup requires a weak reference to our object.
* It's easier to create it here than in C++.
@@ -275,6 +283,7 @@ public class MediaRecorder implements AudioRouting
* third-party applications.
* </p>
*/
+ @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT)
public static final int REMOTE_SUBMIX = 8;
/** Microphone audio source tuned for unprocessed (raw) sound if available, behaves like
@@ -300,6 +309,7 @@ public class MediaRecorder implements AudioRouting
* @hide
*/
@SystemApi
+ @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
public static final int HOTWORD = 1999;
}
@@ -749,6 +759,7 @@ public class MediaRecorder implements AudioRouting
if (numChannels <= 0) {
throw new IllegalArgumentException("Number of channels is not positive");
}
+ mChannelCount = numChannels;
setParameter("audio-param-number-of-channels=" + numChannels);
}
@@ -1350,6 +1361,7 @@ public class MediaRecorder implements AudioRouting
/*
* 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);
@@ -1406,6 +1418,45 @@ public class MediaRecorder implements AudioRouting
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);
+
+ // Use routed device when there is not information returned by hal.
+ if (activeMicrophones.size() == 0) {
+ AudioDeviceInfo device = getRoutedDevice();
+ if (device != null) {
+ MicrophoneInfo microphone = AudioManager.microphoneInfoFromAudioDeviceInfo(device);
+ ArrayList<Pair<Integer, Integer>> channelMapping = new ArrayList<>();
+ for (int i = 0; i < mChannelCount; i++) {
+ channelMapping.add(new Pair(i, MicrophoneInfo.CHANNEL_MAPPING_DIRECT));
+ }
+ microphone.setChannelMapping(channelMapping);
+ activeMicrophones.add(microphone);
+ }
+ }
+ 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/android/media/MediaScanner.java b/android/media/MediaScanner.java
index cb4e46fe..f476a6cc 100644
--- a/android/media/MediaScanner.java
+++ b/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 = {
@@ -323,7 +324,6 @@ public class MediaScanner implements AutoCloseable {
private final Uri mAudioUri;
private final Uri mVideoUri;
private final Uri mImagesUri;
- private final Uri mThumbsUri;
private final Uri mPlaylistsUri;
private final Uri mFilesUri;
private final Uri mFilesUriNoNotify;
@@ -419,7 +419,6 @@ public class MediaScanner implements AutoCloseable {
mAudioUri = Audio.Media.getContentUri(volumeName);
mVideoUri = Video.Media.getContentUri(volumeName);
mImagesUri = Images.Media.getContentUri(volumeName);
- mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
mFilesUri = Files.getContentUri(volumeName);
mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
@@ -947,6 +946,7 @@ public class MediaScanner implements AutoCloseable {
values.put(Audio.Media.IS_MUSIC, music);
values.put(Audio.Media.IS_PODCAST, podcasts);
} else if ((mFileType == MediaFile.FILE_TYPE_JPEG
+ || mFileType == MediaFile.FILE_TYPE_HEIF
|| MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
ExifInterface exif = null;
try {
@@ -1153,7 +1153,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;
@@ -1283,53 +1286,6 @@ public class MediaScanner implements AutoCloseable {
}
}
- private void pruneDeadThumbnailFiles() {
- HashSet<String> existingFiles = new HashSet<String>();
- String directory = "/sdcard/DCIM/.thumbnails";
- String [] files = (new File(directory)).list();
- Cursor c = null;
- if (files == null)
- files = new String[0];
-
- for (int i = 0; i < files.length; i++) {
- String fullPathString = directory + "/" + files[i];
- existingFiles.add(fullPathString);
- }
-
- try {
- c = mMediaProvider.query(
- mThumbsUri,
- new String [] { "_data" },
- null,
- null,
- null, null);
- Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
- if (c != null && c.moveToFirst()) {
- do {
- String fullPathString = c.getString(0);
- existingFiles.remove(fullPathString);
- } while (c.moveToNext());
- }
-
- for (String fileToDelete : existingFiles) {
- if (false)
- Log.v(TAG, "fileToDelete is " + fileToDelete);
- try {
- (new File(fileToDelete)).delete();
- } catch (SecurityException ex) {
- }
- }
-
- Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
- } catch (RemoteException e) {
- // We will soon be killed...
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
static class MediaBulkDeleter {
StringBuilder whereClause = new StringBuilder();
ArrayList<String> whereArgs = new ArrayList<String>(100);
@@ -1373,9 +1329,6 @@ public class MediaScanner implements AutoCloseable {
processPlayLists();
}
- if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
- pruneDeadThumbnailFiles();
-
// allow GC to clean up
mPlayLists.clear();
}
diff --git a/android/media/MediaSession2.java b/android/media/MediaSession2.java
index 0e90040a..2b3c2b4c 100644
--- a/android/media/MediaSession2.java
+++ b/android/media/MediaSession2.java
@@ -16,36 +16,37 @@
package android.media;
+import static android.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
-import android.media.MediaPlayerBase.PlaybackListener;
-import android.media.session.MediaSession;
-import android.media.session.MediaSession.Callback;
-import android.media.session.PlaybackState;
+import android.media.MediaPlayerBase.BuffState;
+import android.media.MediaPlayerBase.PlayerState;
+import android.media.MediaPlaylistAgent.RepeatMode;
+import android.media.MediaPlaylistAgent.ShuffleMode;
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.ControllerInfoProvider;
+import android.media.update.ProviderCreator;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Parcelable;
+import android.os.IInterface;
import android.os.ResultReceiver;
-import android.text.TextUtils;
-import android.util.ArraySet;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
+ * @hide
* 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>
@@ -59,7 +60,7 @@ import java.util.concurrent.Executor;
* 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
+ * instead. With it, your playback can be revived even after playback is finished. See
* {@link MediaSessionService2} for details.
* <p>
* A session can be obtained by {@link Builder}. The owner of the session may pass its session token
@@ -67,7 +68,8 @@ import java.util.concurrent.Executor;
* 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(MediaPlayerBase)}.
+ * the the underlying media player set by {@link Builder} or
+ * {@link #updatePlayer}.
* <p>
* When an app is finished performing playback it must call {@link #close()} to clean up the session
* and notify any controllers.
@@ -75,228 +77,110 @@ import java.util.concurrent.Executor;
* {@link MediaSession2} objects should be used on the thread on the looper.
*
* @see MediaSessionService2
- * @hide
*/
-// TODO(jaewan): Unhide
-// TODO(jaewan): Revisit comments. Currently it's borrowed from the MediaSession.
-// TODO(jaewan): Should we support thread safe? It may cause tricky issue such as b/63797089
-// TODO(jaewan): Should we make APIs for MediaSessionService2 public? It's helpful for
-// developers that doesn't want to override from Browser, but user may not use this
-// correctly.
public class MediaSession2 implements AutoCloseable {
private final MediaSession2Provider mProvider;
- // Note: Do not define IntDef because subclass can add more command code on top of these.
- // TODO(jaewan): Shouldn't we pull out?
- public static final int COMMAND_CODE_CUSTOM = 0;
- public static final int COMMAND_CODE_PLAYBACK_START = 1;
- public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2;
- public static final int COMMAND_CODE_PLAYBACK_STOP = 3;
- public static final int COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM = 4;
- public static final int COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM = 5;
- public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
- public static final int COMMAND_CODE_PLAYBACK_FAST_FORWARD = 7;
- public static final int COMMAND_CODE_PLAYBACK_REWIND = 8;
- public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
- public static final int COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM = 10;
-
- public static final int COMMAND_CODE_PLAYLIST_GET = 11;
- public static final int COMMAND_CODE_PLAYLIST_ADD = 12;
- public static final int COMMAND_CODE_PLAYLIST_REMOVE = 13;
-
- public static final int COMMAND_CODE_PLAY_FROM_MEDIA_ID = 14;
- public static final int COMMAND_CODE_PLAY_FROM_URI = 15;
- public static final int COMMAND_CODE_PLAY_FROM_SEARCH = 16;
-
- public static final int COMMAND_CODE_PREPARE_FROM_MEDIA_ID = 17;
- public static final int COMMAND_CODE_PREPARE_FROM_URI = 18;
- public static final int COMMAND_CODE_PREPARE_FROM_SEARCH = 19;
-
/**
- * 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}.
+ * @hide
*/
- // TODO(jaewan): Move this into the updatable.
- public static final class Command {
- private static final String KEY_COMMAND_CODE
- = "android.media.media_session2.command.command_code";
- private static final String KEY_COMMAND_CUSTOM_COMMAND
- = "android.media.media_session2.command.custom_command";
- private static final String KEY_COMMAND_EXTRA
- = "android.media.media_session2.command.extra";
-
- private final int mCommandCode;
- // Nonnull if it's custom command
- private final String mCustomCommand;
- private final Bundle mExtra;
-
- public Command(int commandCode) {
- mCommandCode = commandCode;
- mCustomCommand = null;
- mExtra = null;
- }
+ @IntDef({ERROR_CODE_UNKNOWN_ERROR, ERROR_CODE_APP_ERROR, ERROR_CODE_NOT_SUPPORTED,
+ ERROR_CODE_AUTHENTICATION_EXPIRED, ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
+ ERROR_CODE_CONCURRENT_STREAM_LIMIT, ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
+ ERROR_CODE_NOT_AVAILABLE_IN_REGION, ERROR_CODE_CONTENT_ALREADY_PLAYING,
+ ERROR_CODE_SKIP_LIMIT_REACHED, ERROR_CODE_ACTION_ABORTED, ERROR_CODE_END_OF_QUEUE,
+ ERROR_CODE_SETUP_REQUIRED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ErrorCode {}
- public Command(@NonNull String action, @Nullable Bundle extra) {
- if (action == null) {
- throw new IllegalArgumentException("action shouldn't be null");
- }
- mCommandCode = COMMAND_CODE_CUSTOM;
- mCustomCommand = action;
- mExtra = extra;
- }
-
- public int getCommandCode() {
- return mCommandCode;
- }
-
- public @Nullable String getCustomCommand() {
- return mCustomCommand;
- }
-
- public @Nullable Bundle getExtra() {
- return mExtra;
- }
-
- /**
- * @return a new Bundle instance from the Command
- * @hide
- */
- public Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putInt(KEY_COMMAND_CODE, mCommandCode);
- bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand);
- bundle.putBundle(KEY_COMMAND_EXTRA, mExtra);
- return bundle;
- }
+ /**
+ * This is the default error code and indicates that none of the other error codes applies.
+ */
+ public static final int ERROR_CODE_UNKNOWN_ERROR = 0;
- /**
- * @return a new Command instance from the Bundle
- * @hide
- */
- public static Command fromBundle(Bundle command) {
- int code = command.getInt(KEY_COMMAND_CODE);
- if (code != COMMAND_CODE_CUSTOM) {
- return new Command(code);
- } else {
- String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND);
- if (customCommand == null) {
- return null;
- }
- return new Command(customCommand, command.getBundle(KEY_COMMAND_EXTRA));
- }
- }
+ /**
+ * Error code when the application state is invalid to fulfill the request.
+ */
+ public static final int ERROR_CODE_APP_ERROR = 1;
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof Command)) {
- return false;
- }
- Command other = (Command) obj;
- // TODO(jaewan): Should we also compare contents in bundle?
- // It may not be possible if the bundle contains private class.
- return mCommandCode == other.mCommandCode
- && TextUtils.equals(mCustomCommand, other.mCustomCommand);
- }
+ /**
+ * Error code when the request is not supported by the application.
+ */
+ public static final int ERROR_CODE_NOT_SUPPORTED = 2;
- @Override
- public int hashCode() {
- final int prime = 31;
- return ((mCustomCommand != null) ? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
- }
- }
+ /**
+ * Error code when the request cannot be performed because authentication has expired.
+ */
+ public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3;
/**
- * Represent set of {@link Command}.
+ * Error code when a premium account is required for the request to succeed.
*/
- // TODO(jaewan): Move this to updatable
- public static class CommandGroup {
- private static final String KEY_COMMANDS =
- "android.media.mediasession2.commandgroup.commands";
- private ArraySet<Command> mCommands = new ArraySet<>();
+ public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4;
- public CommandGroup() {
- }
+ /**
+ * Error code when too many concurrent streams are detected.
+ */
+ public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5;
- public CommandGroup(CommandGroup others) {
- mCommands.addAll(others.mCommands);
- }
+ /**
+ * Error code when the content is blocked due to parental controls.
+ */
+ public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6;
- public void addCommand(Command command) {
- mCommands.add(command);
- }
+ /**
+ * Error code when the content is blocked due to being regionally unavailable.
+ */
+ public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7;
- public void addAllPredefinedCommands() {
- // TODO(jaewan): Is there any better way than this?
- mCommands.add(new Command(COMMAND_CODE_PLAYBACK_START));
- mCommands.add(new Command(COMMAND_CODE_PLAYBACK_PAUSE));
- mCommands.add(new Command(COMMAND_CODE_PLAYBACK_STOP));
- mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM));
- mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM));
- }
+ /**
+ * Error code when the requested content is already playing.
+ */
+ public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8;
- public void removeCommand(Command command) {
- mCommands.remove(command);
- }
+ /**
+ * Error code when the application cannot skip any more songs because skip limit is reached.
+ */
+ public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9;
- public boolean hasCommand(Command command) {
- return mCommands.contains(command);
- }
+ /**
+ * Error code when the action is interrupted due to some external event.
+ */
+ public static final int ERROR_CODE_ACTION_ABORTED = 10;
- public boolean hasCommand(int code) {
- if (code == COMMAND_CODE_CUSTOM) {
- throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
- }
- for (int i = 0; i < mCommands.size(); i++) {
- if (mCommands.valueAt(i).getCommandCode() == code) {
- return true;
- }
- }
- return false;
- }
+ /**
+ * Error code when the playback navigation (previous, next) is not possible because the queue
+ * was exhausted.
+ */
+ public static final int ERROR_CODE_END_OF_QUEUE = 11;
- /**
- * @return new bundle from the CommandGroup
- * @hide
- */
- public Bundle toBundle() {
- ArrayList<Bundle> list = new ArrayList<>();
- for (int i = 0; i < mCommands.size(); i++) {
- list.add(mCommands.valueAt(i).toBundle());
- }
- Bundle bundle = new Bundle();
- bundle.putParcelableArrayList(KEY_COMMANDS, list);
- return bundle;
- }
+ /**
+ * Error code when the session needs user's manual intervention.
+ */
+ public static final int ERROR_CODE_SETUP_REQUIRED = 12;
+ /**
+ * Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist
+ * didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it.
+ *
+ * #see #setOnDataSourceMissingHelper
+ */
+ public interface OnDataSourceMissingHelper {
/**
- * @return new instance of CommandGroup from the bundle
- * @hide
+ * Called when a {@link MediaItem2} in the playlist didn't have a {@link DataSourceDesc}
+ * but it's needed now for preparing or playing it. Returned data source descriptor will be
+ * sent to the player directly to prepare or play the contents.
+ * <p>
+ * An exception may be thrown if the returned {@link DataSourceDesc} is duplicated in the
+ * playlist, so items cannot be differentiated.
+ *
+ * @param session the session for this event
+ * @param item media item from the controller
+ * @return a data source descriptor if the media item. Can be {@code null} if the content
+ * isn't available.
*/
- public static @Nullable CommandGroup fromBundle(Bundle commands) {
- if (commands == null) {
- return null;
- }
- List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS);
- if (list == null) {
- return null;
- }
- CommandGroup commandGroup = new CommandGroup();
- for (int i = 0; i < list.size(); i++) {
- Parcelable parcelable = list.get(i);
- if (!(parcelable instanceof Bundle)) {
- continue;
- }
- Bundle commandBundle = (Bundle) parcelable;
- Command command = Command.fromBundle(commandBundle);
- if (command != null) {
- commandGroup.addCommand(command);
- }
- }
- return commandGroup;
- }
+ @Nullable DataSourceDesc onDataSourceMissing(@NonNull MediaSession2 session,
+ @NonNull MediaItem2 item);
}
/**
@@ -305,21 +189,23 @@ public class MediaSession2 implements AutoCloseable {
* 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 {
+ // TODO(jaewan): Move this to updatable for default implementation (b/74091963)
+ public static abstract class SessionCallback {
/**
* 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.
+ * {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)} and cannot
+ * be usable.
*
+ * @param session the session for this event
* @param controller controller information.
- * @return allowed commands. Can be {@code null} to reject coonnection.
+ * @return allowed commands. Can be {@code null} to reject connection.
*/
- // TODO(jaewan): Change return type. Once we do, null is for reject.
- public @Nullable CommandGroup onConnect(@NonNull ControllerInfo controller) {
- CommandGroup commands = new CommandGroup();
+ public @Nullable SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
commands.addAllPredefinedCommands();
return commands;
}
@@ -327,213 +213,395 @@ public class MediaSession2 implements AutoCloseable {
/**
* Called when a controller is disconnected
*
+ * @param session the session for this event
* @param controller controller information
*/
- public void onDisconnected(@NonNull ControllerInfo controller) { }
+ public void onDisconnected(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) { }
/**
- * Called when a controller sent a command to the session, and the command will be sent to
- * the player directly unless you reject the request by {@code false}.
+ * 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 session the session for this event
* @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 SessionCommand2#COMMAND_CODE_PLAYBACK_PLAY
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PAUSE
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_STOP
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_PREV_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PREPARE
+ * @see SessionCommand2#COMMAND_CODE_SESSION_FAST_FORWARD
+ * @see SessionCommand2#COMMAND_CODE_SESSION_REWIND
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_SEEK_TO
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REMOVE_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
+ * @see SessionCommand2#COMMAND_CODE_SET_VOLUME
*/
- // TODO(jaewan): Add more documentations (or make it clear) which commands can be filtered
- // with this.
- public boolean onCommandRequest(@NonNull ControllerInfo controller,
- @NonNull Command command) {
+ public boolean onCommandRequest(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull SessionCommand2 command) {
return true;
}
/**
- * Called when a controller set rating on the currently playing contents.
+ * Called when a controller set rating of a media item through
+ * {@link MediaController2#setRating(String, Rating2)}.
+ * <p>
+ * To allow setting user rating for a {@link MediaItem2}, the media item's metadata
+ * should have {@link Rating2} with the key {@link MediaMetadata#METADATA_KEY_USER_RATING},
+ * in order to provide possible rating style for controller. Controller will follow the
+ * rating style.
*
- * @param
+ * @param session the session for this event
+ * @param controller controller information
+ * @param mediaId media id from the controller
+ * @param rating new rating from the controller
*/
- public void onSetRating(@NonNull ControllerInfo controller, @NonNull Rating2 rating) { }
+ public void onSetRating(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
+ @NonNull String mediaId, @NonNull Rating2 rating) { }
/**
- * Called when a controller sent a custom command.
+ * Called when a controller sent a custom command through
+ * {@link MediaController2#sendCustomCommand(SessionCommand2, Bundle, ResultReceiver)}.
*
+ * @param session the session for this event
* @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) { }
+ public void onCustomCommand(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull SessionCommand2 customCommand,
+ @Nullable Bundle args, @Nullable ResultReceiver cb) { }
+
+ /**
+ * Called when a controller requested to play a specific mediaId through
+ * {@link MediaController2#playFromMediaId(String, Bundle)}.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param mediaId media id
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID
+ */
+ public void onPlayFromMediaId(@NonNull MediaSession2 session,
+ @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 session the session for this event
+ * @param controller controller information
+ * @param query query string. Can be empty to indicate any suggested media
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH
+ */
+ public void onPlayFromSearch(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull String query,
+ @Nullable Bundle extras) { }
/**
- * Override to handle requests to prepare for playing a specific mediaId.
+ * Called when a controller requested to play a specific media item represented by a URI
+ * through {@link MediaController2#playFromUri(Uri, Bundle)}
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param uri uri
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_URI
+ */
+ public void onPlayFromUri(@NonNull MediaSession2 session,
+ @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.
+ * {@link MediaPlayerBase#PLAYER_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 session the session for this event
+ * @param controller controller information
+ * @param mediaId media id to prepare
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID
*/
- public void onPlayFromMediaId(@NonNull ControllerInfo controller,
- @NonNull String mediaId, @Nullable Bundle extras) { }
+ public void onPrepareFromMediaId(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull String mediaId,
+ @Nullable Bundle extras) { }
/**
- * Override to handle requests to prepare playback from a search query. An empty query
- * indicates that the app may prepare any music. The implementation should attempt to make a
- * smart choice about what to play. 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.
+ * Called when a controller requested to prepare playback from a search query through
+ * {@link MediaController2#prepareFromSearch(String, Bundle)}.
* <p>
- * The playback of the prepared content should start in the later calls of
- * {@link MediaSession2#play()}.
+ * 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 MediaPlayerBase#PLAYER_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 session the session for this event
+ * @param controller controller information
+ * @param query query string. Can be empty to indicate any suggested media
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH
*/
- public void onPlayFromSearch(@NonNull ControllerInfo controller,
- @NonNull String query, @Nullable Bundle extras) { }
+ public void onPrepareFromSearch(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull String query,
+ @Nullable Bundle extras) { }
/**
- * Override to handle requests to prepare a specific media item represented by a URI.
+ * Called when a controller requested to prepare a specific media item represented by a URI
+ * through {@link MediaController2#prepareFromUri(Uri, 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.
+ * {@link MediaPlayerBase#PLAYER_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 session the session for this event
+ * @param controller controller information
+ * @param uri uri
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_URI
*/
- public void onPlayFromUri(@NonNull ControllerInfo controller,
- @NonNull String uri, @Nullable Bundle extras) { }
+ public void onPrepareFromUri(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) { }
/**
- * Override to handle requests to play a specific mediaId.
+ * Called when a controller called {@link MediaController2#fastForward()}
+ *
+ * @param session the session for this event
*/
- public void onPrepareFromMediaId(@NonNull ControllerInfo controller,
- @NonNull String mediaId, @Nullable Bundle extras) { }
+ public void onFastForward(@NonNull MediaSession2 session) { }
/**
- * Override to handle requests to begin playback from a search query. An
- * empty query indicates that the app may play any music. The
- * implementation should attempt to make a smart choice about what to
- * play.
+ * Called when a controller called {@link MediaController2#rewind()}
+ *
+ * @param session the session for this event
+ */
+ public void onRewind(@NonNull MediaSession2 session) { }
+
+ /**
+ * Called when the player's current playing item is changed
+ * <p>
+ * When it's called, you should invalidate previous playback information and wait for later
+ * callbacks.
+ *
+ * @param session the controller for this event
+ * @param player the player for this event
+ * @param item new item
*/
- public void onPrepareFromSearch(@NonNull ControllerInfo controller,
- @NonNull String query, @Nullable Bundle extras) { }
+ // TODO(jaewan): Use this (b/74316764)
+ public void onCurrentMediaItemChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { }
/**
- * Override to handle requests to play a specific media item represented by a URI.
+ * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
+ * referenced by the given data source.
+ * @param session the session for this event
+ * @param player the player for this event
+ * @param item the media item for which buffering is happening
*/
- public void prepareFromUri(@NonNull ControllerInfo controller,
- @NonNull Uri uri, @Nullable Bundle extras) { }
+ public void onMediaPrepared(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
+ @NonNull MediaItem2 item) { }
/**
- * Called when a controller wants to add a {@link MediaItem2} at the specified position
- * in the play queue.
+ * Called to indicate that the state of the player has changed.
+ * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+ * @param session the session for this event
+ * @param player the player for this event
+ * @param state the new state of the player.
+ */
+ public void onPlayerStateChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player, @PlayerState int state) { }
+
+ /**
+ * Called to report buffering events for a data source.
+ *
+ * @param session the session for this event
+ * @param player the player for this event
+ * @param item the media item for which buffering is happening.
+ * @param state the new buffering state.
+ */
+ public void onBufferingStateChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player, @NonNull MediaItem2 item, @BuffState int state) { }
+
+ /**
+ * Called to indicate that the playback speed has changed.
+ * @param session the session for this event
+ * @param player the player for this event
+ * @param speed the new playback speed.
+ */
+ public void onPlaybackSpeedChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player, float speed) { }
+
+ /**
+ * Called to indicate that {@link #seekTo(long)} is completed.
+ *
+ * @param session the session for this event.
+ * @param mpb the player that has completed seeking.
+ * @param position the previous seeking request.
+ * @see #seekTo(long)
+ */
+ public void onSeekCompleted(@NonNull MediaSession2 session, @NonNull MediaPlayerBase mpb,
+ long position) { }
+
+ /**
+ * Called when a playlist is changed from the {@link MediaPlaylistAgent}.
* <p>
- * The item from the media controller wouldn't have valid data source descriptor because
- * it would have been anonymized when it's sent to the remote process.
+ * This is called when the underlying agent has called
+ * {@link MediaPlaylistAgent.PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent,
+ * List, MediaMetadata2)}.
+ *
+ * @param session the session for this event
+ * @param playlistAgent playlist agent for this event
+ * @param list new playlist
+ * @param metadata new metadata
+ */
+ public void onPlaylistChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlaylistAgent playlistAgent, @NonNull List<MediaItem2> list,
+ @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when a playlist metadata is changed.
*
- * @param item The media item to be inserted.
- * @param index The index at which the item is to be inserted.
+ * @param session the session for this event
+ * @param playlistAgent playlist agent for this event
+ * @param metadata new metadata
*/
- public void onAddPlaylistItem(@NonNull ControllerInfo controller,
- @NonNull MediaItem2 item, int index) { }
+ public void onPlaylistMetadataChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlaylistAgent playlistAgent, @Nullable MediaMetadata2 metadata) { }
/**
- * Called when a controller wants to remove the {@link MediaItem2}
+ * Called when the shuffle mode is changed.
*
- * @param item
+ * @param session the session for this event
+ * @param playlistAgent playlist agent for this event
+ * @param shuffleMode repeat mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
*/
- // Can we do this automatically?
- public void onRemovePlaylistItem(@NonNull MediaItem2 item) { }
- };
+ public void onShuffleModeChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlaylistAgent playlistAgent,
+ @MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
+
+ /**
+ * Called when the repeat mode is changed.
+ *
+ * @param session the session for this event
+ * @param playlistAgent playlist agent for this event
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public void onRepeatModeChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlaylistAgent playlistAgent,
+ @MediaPlaylistAgent.RepeatMode int repeatMode) { }
+ }
/**
- * Base builder class for MediaSession2 and its subclass.
- *
+ * 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.MediaLibrarySession.Builder}.
+ * <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.BuilderBase<T, C>, C extends SessionCallback> {
- final Context mContext;
- final MediaPlayerBase mPlayer;
- String mId;
- Executor mCallbackExecutor;
- C mCallback;
- VolumeProvider mVolumeProvider;
- int mRatingType;
- PendingIntent mSessionActivity;
+ <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);
+ }
/**
- * Constructor.
+ * Sets the underlying {@link MediaPlayerBase} for this session to dispatch incoming event
+ * to.
*
- * @param context a context
- * @param player a player to handle incoming command from any controller.
- * @throws IllegalArgumentException if any parameter is null, or the player is a
- * {@link MediaSession2} or {@link MediaController2}.
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
*/
- // TODO(jaewan): Also need executor
- public BuilderBase(@NonNull Context context, @NonNull MediaPlayerBase player) {
- if (context == null) {
- throw new IllegalArgumentException("context shouldn't be null");
- }
- if (player == null) {
- throw new IllegalArgumentException("player shouldn't be null");
- }
- mContext = context;
- mPlayer = player;
- // Ensure non-null
- mId = "";
+ @NonNull U setPlayer(@NonNull MediaPlayerBase player) {
+ mProvider.setPlayer_impl(player);
+ return (U) 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.
+ * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
+ * underlying {@link MediaPlayerBase}. The playlist agent should manage
+ * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
* <p>
- * Set {@code null} to reset.
+ * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
+ * agent.
*
- * @param volumeProvider The provider that will handle volume changes. Can be {@code null}
+ * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
+ * {@code player}
*/
- public T setVolumeProvider(@Nullable VolumeProvider volumeProvider) {
- mVolumeProvider = volumeProvider;
- return (T) this;
+ U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+ mProvider.setPlaylistAgent_impl(playlistAgent);
+ 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>
+ * Sets the {@link VolumeProvider2} for this session to handle volume events. If not set,
+ * system will adjust the appropriate stream volume for this session's player.
+ *
+ * @param volumeProvider The provider that will receive volume button events.
*/
- public T setRatingType(@Rating2.Style int type) {
- mRatingType = type;
- return (T) this;
+ @NonNull U setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) {
+ mProvider.setVolumeProvider_impl(volumeProvider);
+ 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 Activity#startActivity(Intent)}.
+ * activity that may be started using {@link Context#startActivity(Intent)}.
*
* @param pi The intent to launch to show UI for this session.
*/
- public T setSessionActivity(@Nullable PendingIntent pi) {
- mSessionActivity = pi;
- return (T) this;
+ @NonNull U setSessionActivity(@Nullable PendingIntent pi) {
+ mProvider.setSessionActivity_impl(pi);
+ return (U) this;
}
/**
@@ -546,32 +614,22 @@ public class MediaSession2 implements AutoCloseable {
* @throws IllegalArgumentException if id is {@code null}
* @return
*/
- public T setId(@NonNull String id) {
- if (id == null) {
- throw new IllegalArgumentException("id shouldn't be null");
- }
- mId = id;
- return (T) this;
+ @NonNull U setId(@NonNull String id) {
+ mProvider.setId_impl(id);
+ return (U) this;
}
/**
- * Set {@link SessionCallback}.
+ * Set callback for the session.
*
* @param executor callback executor
* @param callback session callback.
* @return
*/
- public T setSessionCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull U setSessionCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull C callback) {
- if (executor == null) {
- throw new IllegalArgumentException("executor shouldn't be null");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback shouldn't be null");
- }
- mCallbackExecutor = executor;
- mCallback = callback;
- return (T) this;
+ mProvider.setSessionCallback_impl(executor, callback);
+ return (U) this;
}
/**
@@ -581,7 +639,9 @@ public class MediaSession2 implements AutoCloseable {
* @throws IllegalStateException if the session with the same id is already exists for the
* package.
*/
- public abstract MediaSession2 build() throws IllegalStateException;
+ @NonNull T build() {
+ return mProvider.build_impl();
+ }
}
/**
@@ -590,47 +650,70 @@ public class MediaSession2 implements AutoCloseable {
* Any incoming event from the {@link MediaController2} will be handled on the thread
* that created session with the {@link Builder#build()}.
*/
- // TODO(jaewan): Move this to updatable
- // TODO(jaewan): Add setRatingType()
- // TODO(jaewan): Add setSessionActivity()
- public static final class Builder extends BuilderBase<Builder, SessionCallback> {
- public Builder(Context context, @NonNull MediaPlayerBase player) {
- super(context, player);
+ // 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) {
+ super((instance) -> ApiLoader.getProvider().createMediaSession2Builder(
+ context, (Builder) instance));
}
@Override
- public MediaSession2 build() throws IllegalStateException {
- if (mCallback == null) {
- mCallback = new SessionCallback();
- }
- return new MediaSession2(mContext, mPlayer, mId, mCallbackExecutor, mCallback,
- mVolumeProvider, mRatingType, mSessionActivity);
+ public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+ return super.setPlayer(player);
+ }
+
+ @Override
+ public Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+ return super.setPlaylistAgent(playlistAgent);
+ }
+
+ @Override
+ public @NonNull Builder setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) {
+ return super.setVolumeProvider(volumeProvider);
+ }
+
+ @Override
+ public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) {
+ return super.setSessionActivity(pi);
+ }
+
+ @Override
+ public @NonNull Builder setId(@NonNull String id) {
+ return super.setId(id);
+ }
+
+ @Override
+ public @NonNull Builder setSessionCallback(@NonNull Executor executor,
+ @Nullable SessionCallback callback) {
+ return super.setSessionCallback(executor, callback);
+ }
+
+ @Override
+ public @NonNull MediaSession2 build() {
+ return super.build();
}
}
/**
* Information of a controller.
*/
- // TODO(jaewan): Move implementation to the updatable.
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,
- IMediaSession2Callback callback) {
- mProvider = ApiLoader.getProvider(context)
- .createMediaSession2ControllerInfoProvider(
- this, context, uid, pid, packageName, callback);
+ public ControllerInfo(@NonNull Context context, int uid, int pid,
+ @NonNull String packageName, @NonNull IInterface callback) {
+ mProvider = ApiLoader.getProvider().createMediaSession2ControllerInfo(
+ context, this, uid, pid, packageName, callback);
}
/**
* @return package name of the controller
*/
- public String getPackageName() {
+ public @NonNull String getPackageName() {
return mProvider.getPackageName_impl();
}
@@ -654,10 +737,8 @@ public class MediaSession2 implements AutoCloseable {
/**
* @hide
- * @return
*/
- // TODO(jaewan): SystemApi
- public ControllerInfoProvider getProvider() {
+ public @NonNull ControllerInfoProvider getProvider() {
return mProvider;
}
@@ -668,52 +749,28 @@ public class MediaSession2 implements AutoCloseable {
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof ControllerInfo)) {
- return false;
- }
- ControllerInfo other = (ControllerInfo) obj;
- return mProvider.equals_impl(other.mProvider);
+ return mProvider.equals_impl(obj);
}
@Override
public String toString() {
- // TODO(jaewan): Move this to updatable.
- return "ControllerInfo {pkg=" + getPackageName() + ", uid=" + getUid() + ", trusted="
- + isTrusted() + "}";
+ return mProvider.toString_impl();
}
}
/**
- * Button for a {@link Command} that will be shown by the controller.
+ * Button for a {@link SessionCommand2} that will be shown by the controller.
* <p>
* It's up to the controller's decision to respect or ignore this customization request.
*/
- // TODO(jaewan): Move this to updatable.
- public static class CommandButton {
- private static final String KEY_COMMAND
- = "android.media.media_session2.command_button.command";
- private static final String KEY_ICON_RES_ID
- = "android.media.media_session2.command_button.icon_res_id";
- private static final String KEY_DISPLAY_NAME
- = "android.media.media_session2.command_button.display_name";
- private static final String KEY_EXTRA
- = "android.media.media_session2.command_button.extra";
- private static final String KEY_ENABLED
- = "android.media.media_session2.command_button.enabled";
-
- private Command mCommand;
- private int mIconResId;
- private String mDisplayName;
- private Bundle mExtra;
- private boolean mEnabled;
-
- private CommandButton(@Nullable Command command, int iconResId,
- @Nullable String displayName, Bundle extra, boolean enabled) {
- mCommand = command;
- mIconResId = iconResId;
- mDisplayName = displayName;
- mExtra = extra;
- mEnabled = enabled;
+ public static final class CommandButton {
+ private final CommandButtonProvider mProvider;
+
+ /**
+ * @hide
+ */
+ public CommandButton(CommandButtonProvider provider) {
+ mProvider = provider;
}
/**
@@ -722,8 +779,9 @@ public class MediaSession2 implements AutoCloseable {
*
* @return command or {@code null}
*/
- public @Nullable Command getCommand() {
- return mCommand;
+ public @Nullable
+ SessionCommand2 getCommand() {
+ return mProvider.getCommand_impl();
}
/**
@@ -733,7 +791,7 @@ public class MediaSession2 implements AutoCloseable {
* @return resource id of the icon. Can be {@code 0}.
*/
public int getIconResId() {
- return mIconResId;
+ return mProvider.getIconResId_impl();
}
/**
@@ -743,7 +801,7 @@ public class MediaSession2 implements AutoCloseable {
* @return custom display name. Can be {@code null} or empty.
*/
public @Nullable String getDisplayName() {
- return mDisplayName;
+ return mProvider.getDisplayName_impl();
}
/**
@@ -751,8 +809,8 @@ public class MediaSession2 implements AutoCloseable {
*
* @return
*/
- public @Nullable Bundle getExtra() {
- return mExtra;
+ public @Nullable Bundle getExtras() {
+ return mProvider.getExtras_impl();
}
/**
@@ -761,181 +819,53 @@ public class MediaSession2 implements AutoCloseable {
* @return {@code true} if enabled. {@code false} otherwise.
*/
public boolean isEnabled() {
- return mEnabled;
+ return mProvider.isEnabled_impl();
}
/**
* @hide
*/
- // TODO(jaewan): @SystemApi
- public @NonNull Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putBundle(KEY_COMMAND, mCommand.toBundle());
- bundle.putInt(KEY_ICON_RES_ID, mIconResId);
- bundle.putString(KEY_DISPLAY_NAME, mDisplayName);
- bundle.putBundle(KEY_EXTRA, mExtra);
- bundle.putBoolean(KEY_ENABLED, mEnabled);
- return bundle;
- }
-
- /**
- * @hide
- */
- // TODO(jaewan): @SystemApi
- public static @Nullable CommandButton fromBundle(Bundle bundle) {
- Builder builder = new Builder();
- builder.setCommand(Command.fromBundle(bundle.getBundle(KEY_COMMAND)));
- builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0));
- builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME));
- builder.setExtra(bundle.getBundle(KEY_EXTRA));
- builder.setEnabled(bundle.getBoolean(KEY_ENABLED));
- try {
- return builder.build();
- } catch (IllegalStateException e) {
- // Malformed or version mismatch. Return null for now.
- return null;
- }
+ public @NonNull CommandButtonProvider getProvider() {
+ return mProvider;
}
/**
* Builder for {@link CommandButton}.
*/
- public static class Builder {
- private Command mCommand;
- private int mIconResId;
- private String mDisplayName;
- private Bundle mExtra;
- private boolean mEnabled;
+ public static final class Builder {
+ private final CommandButtonProvider.BuilderProvider mProvider;
public Builder() {
- mEnabled = true;
+ mProvider = ApiLoader.getProvider().createMediaSession2CommandButtonBuilder(this);
}
- public Builder setCommand(Command command) {
- mCommand = command;
- return this;
+ public @NonNull Builder setCommand(@Nullable SessionCommand2 command) {
+ return mProvider.setCommand_impl(command);
}
- public Builder setIconResId(int resId) {
- mIconResId = resId;
- return this;
+ public @NonNull Builder setIconResId(int resId) {
+ return mProvider.setIconResId_impl(resId);
}
- public Builder setDisplayName(String displayName) {
- mDisplayName = displayName;
- return this;
+ public @NonNull Builder setDisplayName(@Nullable String displayName) {
+ return mProvider.setDisplayName_impl(displayName);
}
- public Builder setEnabled(boolean enabled) {
- mEnabled = enabled;
- return this;
+ public @NonNull Builder setEnabled(boolean enabled) {
+ return mProvider.setEnabled_impl(enabled);
}
- public Builder setExtra(Bundle extra) {
- mExtra = extra;
- return this;
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ return mProvider.setExtras_impl(extras);
}
- public CommandButton build() {
- if (mEnabled && mCommand == null) {
- throw new IllegalStateException("Enabled button needs Command"
- + " for controller to invoke the command");
- }
- if (mCommand != null && mCommand.getCommandCode() == COMMAND_CODE_CUSTOM
- && (mIconResId == 0 || TextUtils.isEmpty(mDisplayName))) {
- throw new IllegalStateException("Custom commands needs icon and"
- + " and name to display");
- }
- return new CommandButton(mCommand, mIconResId, mDisplayName, mExtra, mEnabled);
+ public @NonNull CommandButton build() {
+ return mProvider.build_impl();
}
}
}
/**
- * Parameter for the playlist.
- */
- // TODO(jaewan): add fromBundle()/toBundle()
- public static class PlaylistParam {
- /**
- * @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 @RepeatMode int mRepeatMode;
- private @ShuffleMode int mShuffleMode;
-
- private MediaMetadata2 mPlaylistMetadata;
-
- public PlaylistParam(@RepeatMode int repeatMode, @ShuffleMode int shuffleMode,
- @Nullable MediaMetadata2 playlistMetadata) {
- mRepeatMode = repeatMode;
- mShuffleMode = shuffleMode;
- mPlaylistMetadata = playlistMetadata;
- }
-
- public @RepeatMode int getRepeatMode() {
- return mRepeatMode;
- }
-
- public @ShuffleMode int getShuffleMode() {
- return mShuffleMode;
- }
-
- public MediaMetadata2 getPlaylistMetadata() {
- return mPlaylistMetadata;
- }
- }
-
- /**
* Constructor is hidden and apps can only instantiate indirectly through {@link Builder}.
* <p>
* This intended behavior and here's the reasons.
@@ -943,66 +873,45 @@ public class MediaSession2 implements AutoCloseable {
* 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
+ * {@link android.media.session.MediaSession} is available after all of
+ * {@link android.media.session.MediaSession#setFlags(int)},
+ * {@link android.media.session.MediaSession#setCallback(
+ * android.media.session.MediaSession.Callback)},
+ * and {@link android.media.session.MediaSession#setActive(boolean)}.
+ * It was common for an app to omit one, so framework had to add heuristics to figure out
+ * which should be the highest priority for handling media key event.
* @hide
*/
- MediaSession2(Context context, MediaPlayerBase player, String id, Executor callbackExecutor,
- SessionCallback callback, VolumeProvider volumeProvider, int ratingType,
- PendingIntent sessionActivity) {
+ public MediaSession2(MediaSession2Provider provider) {
super();
- mProvider = createProvider(context, player, id, callbackExecutor, callback,
- volumeProvider, ratingType, sessionActivity);
- }
-
- MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id,
- Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
- int ratingType, PendingIntent sessionActivity) {
- return ApiLoader.getProvider(context)
- .createMediaSession2(this, context, player, id, callbackExecutor,
- callback, volumeProvider, ratingType, sessionActivity);
+ mProvider = provider;
}
/**
* @hide
*/
- // TODO(jaewan): SystemApi
- public MediaSession2Provider getProvider() {
+ public @NonNull MediaSession2Provider getProvider() {
return mProvider;
}
/**
- * Set the underlying {@link MediaPlayerBase} 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.
+ * Sets the underlying {@link MediaPlayerBase} and {@link MediaPlaylistAgent} for this session
+ * to dispatch incoming event to.
* <p>
- * If the new player is successfully set, {@link PlaybackListener}
- * will be called to tell the current playback state of the new player.
+ * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
+ * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
* <p>
- * You can also specify a volume provider. If so, playback in the player is considered as
- * remote playback.
- *
- * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
- * @throws IllegalArgumentException if the player is {@code null}.
- */
- public void setPlayer(@NonNull MediaPlayerBase player) {
- mProvider.setPlayer_impl(player);
- }
-
- /**
- * Set the underlying {@link MediaPlayerBase} with the volume provider for remote playback.
+ * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
+ * agent.
*
- * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
- * @param volumeProvider a volume provider
- * @see #setPlayer(MediaPlayerBase)
- * @see Builder#setVolumeProvider(VolumeProvider)
- * @throws IllegalArgumentException if a parameter is {@code null}.
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app
+ * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
+ * @param volumeProvider a {@link VolumeProvider2}. If {@code null}, system will adjust the
+ * appropriate stream volume for this session's player.
*/
- public void setPlayer(@NonNull MediaPlayerBase player, @NonNull VolumeProvider volumeProvider)
- throws IllegalArgumentException {
- mProvider.setPlayer_impl(player, volumeProvider);
+ public void updatePlayer(@NonNull MediaPlayerBase player,
+ @Nullable MediaPlaylistAgent playlistAgent, @Nullable VolumeProvider2 volumeProvider) {
+ mProvider.updatePlayer_impl(player, playlistAgent, volumeProvider);
}
@Override
@@ -1013,11 +922,25 @@ public class MediaSession2 implements AutoCloseable {
/**
* @return player
*/
- public @Nullable MediaPlayerBase getPlayer() {
+ public @NonNull MediaPlayerBase getPlayer() {
return mProvider.getPlayer_impl();
}
/**
+ * @return playlist agent
+ */
+ public @NonNull MediaPlaylistAgent getPlaylistAgent() {
+ return mProvider.getPlaylistAgent_impl();
+ }
+
+ /**
+ * @return volume provider
+ */
+ public @Nullable VolumeProvider2 getVolumeProvider() {
+ return mProvider.getVolumeProvider_impl();
+ }
+
+ /**
* Returns the {@link SessionToken2} for creating {@link MediaController2}.
*/
public @NonNull
@@ -1030,31 +953,13 @@ public class MediaSession2 implements AutoCloseable {
}
/**
- * Sets the {@link AudioAttributes} to be used during the playback of the video.
- *
- * @param attributes non-null <code>AudioAttributes</code>.
- */
- public void setAudioAttributes(@NonNull AudioAttributes attributes) {
- mProvider.setAudioAttributes_impl(attributes);
- }
-
- /**
- * 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.
+ * Set the {@link AudioFocusRequest} to obtain the audio focus
*
- * @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.
+ * @param afr the full request parameters
*/
- public void setAudioFocusRequest(int focusGain) {
- mProvider.setAudioFocusRequest_impl(focusGain);
+ public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
+ // TODO(jaewan): implement this (b/72529899)
+ // mProvider.setAudioFocusRequest_impl(focusGain);
}
/**
@@ -1071,10 +976,11 @@ public class MediaSession2 implements AutoCloseable {
* 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)}.
+ * This API can be called in the {@link SessionCallback#onConnect(
+ * MediaSession2, ControllerInfo)}.
*
* @param controller controller to specify layout.
- * @param layout oredered list of layout.
+ * @param layout ordered list of layout.
*/
public void setCustomLayout(@NonNull ControllerInfo controller,
@NonNull List<CommandButton> layout) {
@@ -1088,25 +994,17 @@ public class MediaSession2 implements AutoCloseable {
* @param commands new allowed commands
*/
public void setAllowedCommands(@NonNull ControllerInfo controller,
- @NonNull CommandGroup commands) {
+ @NonNull SessionCommandGroup2 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) {
+ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
mProvider.sendCustomCommand_impl(command, args);
}
@@ -1117,107 +1015,380 @@ public class MediaSession2 implements AutoCloseable {
* @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) {
+ public void sendCustomCommand(@NonNull ControllerInfo controller,
+ @NonNull SessionCommand2 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
+ * <p>
+ * This calls {@link MediaPlayerBase#play()}.
*/
public void play() {
mProvider.play_impl();
}
/**
- * Pause playback
+ * Pause playback.
+ * <p>
+ * This calls {@link MediaPlayerBase#pause()}.
*/
public void pause() {
mProvider.pause_impl();
}
/**
- * Stop playback
+ * Stop playback, and reset the player to the initial state.
+ * <p>
+ * This calls {@link MediaPlayerBase#reset()}.
*/
public void stop() {
mProvider.stop_impl();
}
/**
- * Rewind playback
+ * 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 MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
+ * to start playback.
+ * <p>
+ * This calls {@link MediaPlayerBase#reset()}.
*/
- public void skipToPrevious() {
- mProvider.skipToPrevious_impl();
+ public void prepare() {
+ mProvider.prepare_impl();
}
/**
- * Rewind playback
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
*/
- public void skipToNext() {
- mProvider.skipToNext_impl();
+ public void seekTo(long pos) {
+ mProvider.seekTo_impl(pos);
}
/**
- * 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.
+ * @hide
*/
- public void prepare() {
- mProvider.prepare_impl();
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
}
/**
- * Start fast forwarding. If playback is already fast forwarding this may increase the rate.
+ * @hide
*/
- public void fastForward() {
- mProvider.fastForward_impl();
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
}
/**
- * Start rewinding. If playback is already rewinding this may increase the rate.
+ * Notify errors to the connected controllers
+ *
+ * @param errorCode error code
+ * @param extras extras
*/
- public void rewind() {
- mProvider.rewind_impl();
+ public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
+ mProvider.notifyError_impl(errorCode, extras);
}
/**
- * Move to a new location in the media stream.
+ * Gets the current player state.
*
- * @param pos Position to move to, in milliseconds.
+ * @return the current player state
*/
- public void seekTo(long pos) {
- mProvider.seekTo_impl(pos);
+ public @PlayerState int getPlayerState() {
+ return mProvider.getPlayerState_impl();
}
/**
- * Sets the index of current DataSourceDesc in the play list to be played.
+ * Gets the current position.
*
- * @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
+ * @return the current playback position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME} if
+ * unknown.
*/
- public void setCurrentPlaylistItem(int index) {
- mProvider.setCurrentPlaylistItem_impl(index);
+ public long getCurrentPosition() {
+ return mProvider.getCurrentPosition_impl();
}
/**
- * @hide
+ * Gets the buffered position, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
+ *
+ * @return the buffered position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
*/
- public void skipForward() {
- // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ public long getBufferedPosition() {
+ return mProvider.getBufferedPosition_impl();
}
/**
- * @hide
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ *
+ * @return the buffering state.
*/
- public void skipBackward() {
- // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ public @BuffState int getBufferingState() {
+ // TODO(jaewan): Implement this
+ return BUFFERING_STATE_UNKNOWN;
+ }
+
+ /**
+ * Get the playback speed.
+ *
+ * @return speed
+ */
+ public float getPlaybackSpeed() {
+ // TODO(jaewan): implement this (b/74093080)
+ return -1;
}
- public void setPlaylist(@NonNull List<MediaItem2> playlist, @NonNull PlaylistParam param) {
- mProvider.setPlaylist_impl(playlist, param);
+ /**
+ * Set the playback speed.
+ */
+ public void setPlaybackSpeed(float speed) {
+ // TODO(jaewan): implement this (b/74093080)
+ }
+
+ /**
+ * Sets the data source missing helper. Helper will be used to provide default implementation of
+ * {@link MediaPlaylistAgent} when it isn't set by developer.
+ * <p>
+ * Default implementation of the {@link MediaPlaylistAgent} will call helper when a
+ * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
+ * when
+ * <ul>
+ * <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
+ * have {@link DataSourceDesc}</li>
+ * <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
+ * by {@link SessionCallback#onCommandRequest(
+ * MediaSession2, ControllerInfo, SessionCommand2)}.
+ * In that case, an item would be added automatically without the data source.</li>
+ * </ul>
+ * <p>
+ * If it's not set, playback wouldn't happen for the item without data source descriptor.
+ * <p>
+ * The helper will be run on the executor that was specified by
+ * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
+ *
+ * @param helper a data source missing helper.
+ * @throws IllegalStateException when the helper is set when the playlist agent is set
+ * @see #setPlaylist(List, MediaMetadata2)
+ * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
+ */
+ public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
+ mProvider.setOnDataSourceMissingHelper_impl(helper);
+ }
+
+ /**
+ * Clears the data source missing helper.
+ *
+ * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
+ */
+ public void clearOnDataSourceMissingHelper() {
+ mProvider.clearOnDataSourceMissingHelper_impl();
+ }
+
+ /**
+ * Returns the playlist from the {@link MediaPlaylistAgent}.
+ * <p>
+ * This list may differ with the list that was specified with
+ * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+ * implementation. Use media items returned here for other playlist agent APIs such as
+ * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+ *
+ * @return playlist
+ * @see MediaPlaylistAgent#getPlaylist()
+ * @see SessionCallback#onPlaylistChanged(
+ * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+ */
+ public List<MediaItem2> getPlaylist() {
+ return mProvider.getPlaylist_impl();
+ }
+
+ /**
+ * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
+ * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
+ * items.
+ * <p>
+ * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
+ * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
+ * List, MediaMetadata2)} to know the operation finishes.
+ * <p>
+ * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
+ * {@link MediaPlaylistAgent} has responsibility to dynamically query {@link DataSourceDesc}
+ * when such media item is ready for preparation or play. Default implementation needs
+ * {@link OnDataSourceMissingHelper} for such case.
+ *
+ * @param list A list of {@link MediaItem2} objects to set as a play list.
+ * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
+ * items.
+ * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
+ * @see SessionCallback#onPlaylistChanged(
+ * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+ * @see #setOnDataSourceMissingHelper
+ */
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ mProvider.setPlaylist_impl(list, metadata);
+ }
+
+ /**
+ * Skips to the item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
+ * on the playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @param item The item in the playlist you want to play
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ */
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ mProvider.skipToPlaylistItem_impl(item);
+ }
+
+ /**
+ * Skips to the previous item.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
+ * playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ **/
+ public void skipToPreviousItem() {
+ mProvider.skipToPreviousItem_impl();
+ }
+
+ /**
+ * Skips to the next item.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
+ * playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ */
+ public void skipToNextItem() {
+ mProvider.skipToNextItem_impl();
+ }
+
+ /**
+ * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
+ *
+ * @return the playlist metadata
+ */
+ public MediaMetadata2 getPlaylistMetadata() {
+ return mProvider.getPlaylistMetadata_impl();
+ }
+
+ /**
+ * Adds the media item to the playlist at position index. Index equals or greater than
+ * the current playlist size will add the item at the end of the playlist.
+ * <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
+ * @param item the media item you want to add
+ */
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ mProvider.addPlaylistItem_impl(index, item);
+ }
+
+ /**
+ * Removes the media item in the playlist.
+ * <p>
+ * If the item is the currently playing item of the playlist, current playback
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @param item the media item you want to add
+ */
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ mProvider.removePlaylistItem_impl(item);
+ }
+
+ /**
+ * Replaces the media item at index in the playlist. This can be also used to update metadata of
+ * an item.
+ *
+ * @param index the index of the item to replace
+ * @param item the new item
+ */
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ mProvider.replacePlaylistItem_impl(index, item);
+ }
+
+ /**
+ * Return currently playing media item.
+ *
+ * @return currently playing media item
+ */
+ public MediaItem2 getCurrentMediaItem() {
+ // TODO(jaewan): Rename provider, and implement (b/74316764)
+ return mProvider.getCurrentPlaylistItem_impl();
+ }
+
+ /**
+ * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
+ *
+ * @param metadata metadata of the playlist
+ */
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ mProvider.updatePlaylistMetadata_impl(metadata);
+ }
+
+ /**
+ * Gets the repeat mode from the {@link MediaPlaylistAgent}.
+ *
+ * @return repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public @RepeatMode int getRepeatMode() {
+ return mProvider.getRepeatMode_impl();
+ }
+
+ /**
+ * Sets the repeat mode to the {@link MediaPlaylistAgent}.
+ *
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ mProvider.setRepeatMode_impl(repeatMode);
+ }
+
+ /**
+ * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
+ *
+ * @return The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public @ShuffleMode int getShuffleMode() {
+ return mProvider.getShuffleMode_impl();
+ }
+
+ /**
+ * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
+ *
+ * @param shuffleMode The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public void setShuffleMode(@ShuffleMode int shuffleMode) {
+ mProvider.setShuffleMode_impl(shuffleMode);
}
}
diff --git a/android/media/MediaSession2Test.java b/android/media/MediaSession2Test.java
deleted file mode 100644
index 045dcd5a..00000000
--- a/android/media/MediaSession2Test.java
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * 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.media.MediaPlayerBase.PlaybackListener;
-import android.media.MediaSession2.Builder;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
-import android.media.session.PlaybackState;
-import android.os.Process;
-import android.os.Looper;
-import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import java.util.ArrayList;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static android.media.TestUtils.createPlaybackState;
-import static org.junit.Assert.*;
-
-/**
- * Tests {@link MediaSession2}.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class MediaSession2Test extends MediaSession2TestBase {
- private static final String TAG = "MediaSession2Test";
-
- private MediaSession2 mSession;
- private MockPlayer mPlayer;
-
- @Before
- @Override
- public void setUp() throws Exception {
- super.setUp();
- sHandler.postAndSync(() -> {
- mPlayer = new MockPlayer(0);
- mSession = new MediaSession2.Builder(mContext, mPlayer).build();
- });
- }
-
- @After
- @Override
- public void cleanUp() throws Exception {
- super.cleanUp();
- sHandler.postAndSync(() -> {
- mSession.close();
- });
- }
-
- @Test
- public void testBuilder() throws Exception {
- try {
- MediaSession2.Builder builder = new Builder(mContext, null);
- fail("null player shouldn't be allowed");
- } catch (IllegalArgumentException e) {
- // expected. pass-through
- }
- MediaSession2.Builder builder = new Builder(mContext, mPlayer);
- try {
- builder.setId(null);
- fail("null id shouldn't be allowed");
- } catch (IllegalArgumentException e) {
- // expected. pass-through
- }
- }
-
- @Test
- public void testSetPlayer() throws Exception {
- sHandler.postAndSync(() -> {
- MockPlayer player = new MockPlayer(0);
- // Test if setPlayer doesn't crash with various situations.
- mSession.setPlayer(mPlayer);
- mSession.setPlayer(player);
- mSession.close();
- });
- }
-
- @Test
- public void testPlay() throws Exception {
- sHandler.postAndSync(() -> {
- mSession.play();
- assertTrue(mPlayer.mPlayCalled);
- });
- }
-
- @Test
- public void testPause() throws Exception {
- sHandler.postAndSync(() -> {
- mSession.pause();
- assertTrue(mPlayer.mPauseCalled);
- });
- }
-
- @Test
- public void testStop() throws Exception {
- sHandler.postAndSync(() -> {
- mSession.stop();
- assertTrue(mPlayer.mStopCalled);
- });
- }
-
- @Test
- public void testSkipToNext() throws Exception {
- sHandler.postAndSync(() -> {
- mSession.skipToNext();
- assertTrue(mPlayer.mSkipToNextCalled);
- });
- }
-
- @Test
- public void testSkipToPrevious() throws Exception {
- sHandler.postAndSync(() -> {
- mSession.skipToPrevious();
- assertTrue(mPlayer.mSkipToPreviousCalled);
- });
- }
-
- @Test
- public void testPlaybackStateChangedListener() throws InterruptedException {
- // TODO(jaewan): Add equivalent tests again
- /*
- final CountDownLatch latch = new CountDownLatch(2);
- final MockPlayer player = new MockPlayer(0);
- final PlaybackListener listener = (state) -> {
- assertEquals(sHandler.getLooper(), Looper.myLooper());
- assertNotNull(state);
- switch ((int) latch.getCount()) {
- case 2:
- assertEquals(PlaybackState.STATE_PLAYING, state.getState());
- break;
- case 1:
- assertEquals(PlaybackState.STATE_PAUSED, state.getState());
- break;
- case 0:
- fail();
- }
- latch.countDown();
- };
- player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
- sHandler.postAndSync(() -> {
- mSession.addPlaybackListener(listener, sHandler);
- // When the player is set, listeners will be notified about the player's current state.
- mSession.setPlayer(player);
- });
- player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
- assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- */
- }
-
- @Test
- public void testBadPlayer() throws InterruptedException {
- // TODO(jaewan): Add equivalent tests again
- /*
- final CountDownLatch latch = new CountDownLatch(3); // expected call + 1
- final BadPlayer player = new BadPlayer(0);
- sHandler.postAndSync(() -> {
- mSession.addPlaybackListener((state) -> {
- // This will be called for every setPlayer() calls, but no more.
- assertNull(state);
- latch.countDown();
- }, sHandler);
- mSession.setPlayer(player);
- mSession.setPlayer(mPlayer);
- });
- player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
- assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- */
- }
-
- private static class BadPlayer extends MockPlayer {
- public BadPlayer(int count) {
- super(count);
- }
-
- @Override
- public void removePlaybackListener(@NonNull PlaybackListener listener) {
- // No-op. This bad player will keep push notification to the listener that is previously
- // registered by session.setPlayer().
- }
- }
-
- @Test
- public void testOnCommandCallback() throws InterruptedException {
- final MockOnCommandCallback callback = new MockOnCommandCallback();
- sHandler.postAndSync(() -> {
- mSession.close();
- mPlayer = new MockPlayer(1);
- mSession = new MediaSession2.Builder(mContext, mPlayer)
- .setSessionCallback(sHandlerExecutor, callback).build();
- });
- MediaController2 controller = createController(mSession.getToken());
- controller.pause();
- assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- assertFalse(mPlayer.mPauseCalled);
- assertEquals(1, callback.commands.size());
- assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE,
- (long) callback.commands.get(0).getCommandCode());
- controller.skipToNext();
- assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- assertTrue(mPlayer.mSkipToNextCalled);
- assertFalse(mPlayer.mPauseCalled);
- assertEquals(2, callback.commands.size());
- assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM,
- (long) callback.commands.get(1).getCommandCode());
- }
-
- @Test
- public void testOnConnectCallback() throws InterruptedException {
- final MockOnConnectCallback sessionCallback = new MockOnConnectCallback();
- sHandler.postAndSync(() -> {
- mSession.close();
- mSession = new MediaSession2.Builder(mContext, mPlayer)
- .setSessionCallback(sHandlerExecutor, sessionCallback).build();
- });
- MediaController2 controller =
- createController(mSession.getToken(), false, null);
- assertNotNull(controller);
- waitForConnect(controller, false);
- waitForDisconnect(controller, true);
- }
-
- public class MockOnConnectCallback extends SessionCallback {
- @Override
- public MediaSession2.CommandGroup onConnect(ControllerInfo controllerInfo) {
- if (Process.myUid() != controllerInfo.getUid()) {
- return null;
- }
- assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
- assertEquals(Process.myUid(), controllerInfo.getUid());
- assertFalse(controllerInfo.isTrusted());
- // Reject all
- return null;
- }
- }
-
- public class MockOnCommandCallback extends SessionCallback {
- public final ArrayList<MediaSession2.Command> commands = new ArrayList<>();
-
- @Override
- public boolean onCommandRequest(ControllerInfo controllerInfo, MediaSession2.Command command) {
- assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
- assertEquals(Process.myUid(), controllerInfo.getUid());
- assertFalse(controllerInfo.isTrusted());
- commands.add(command);
- if (command.getCommandCode() == MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE) {
- return false;
- }
- return true;
- }
- }
-}
diff --git a/android/media/MediaSession2TestBase.java b/android/media/MediaSession2TestBase.java
deleted file mode 100644
index 96afcb90..00000000
--- a/android/media/MediaSession2TestBase.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * 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 static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import android.content.Context;
-import android.media.MediaController2.ControllerCallback;
-import android.media.MediaSession2.CommandGroup;
-import android.os.Bundle;
-import android.os.HandlerThread;
-import android.support.annotation.CallSuper;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.test.InstrumentationRegistry;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-/**
- * Base class for session test.
- */
-abstract class MediaSession2TestBase {
- // Expected success
- static final int WAIT_TIME_MS = 1000;
-
- // Expected timeout
- static final int TIMEOUT_MS = 500;
-
- static TestUtils.SyncHandler sHandler;
- static Executor sHandlerExecutor;
-
- Context mContext;
- private List<MediaController2> mControllers = new ArrayList<>();
-
- interface TestControllerInterface {
- ControllerCallback getCallback();
- }
-
- interface TestControllerCallbackInterface {
- // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
-
- // Browser specific callbacks
- default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
- }
-
- interface WaitForConnectionInterface {
- void waitForConnect(boolean expect) throws InterruptedException;
- void waitForDisconnect(boolean expect) throws InterruptedException;
- }
-
- @BeforeClass
- public static void setUpThread() {
- if (sHandler == null) {
- HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
- handlerThread.start();
- sHandler = new TestUtils.SyncHandler(handlerThread.getLooper());
- sHandlerExecutor = (runnable) -> {
- sHandler.post(runnable);
- };
- }
- }
-
- @AfterClass
- public static void cleanUpThread() {
- if (sHandler != null) {
- sHandler.getLooper().quitSafely();
- sHandler = null;
- sHandlerExecutor = null;
- }
- }
-
- @CallSuper
- public void setUp() throws Exception {
- mContext = InstrumentationRegistry.getTargetContext();
- }
-
- @CallSuper
- public void cleanUp() throws Exception {
- for (int i = 0; i < mControllers.size(); i++) {
- mControllers.get(i).close();
- }
- }
-
- final MediaController2 createController(SessionToken2 token) throws InterruptedException {
- return createController(token, true, null);
- }
-
- final MediaController2 createController(@NonNull SessionToken2 token,
- boolean waitForConnect, @Nullable TestControllerCallbackInterface callback)
- throws InterruptedException {
- TestControllerInterface instance = onCreateController(token, callback);
- if (!(instance instanceof MediaController2)) {
- throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
- + instance);
- }
- MediaController2 controller = (MediaController2) instance;
- mControllers.add(controller);
- if (waitForConnect) {
- waitForConnect(controller, true);
- }
- return controller;
- }
-
- private static WaitForConnectionInterface getWaitForConnectionInterface(
- MediaController2 controller) {
- if (!(controller instanceof TestControllerInterface)) {
- throw new RuntimeException("Test has a bug. Expected controller implemented"
- + " TestControllerInterface but got " + controller);
- }
- ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
- if (!(callback instanceof WaitForConnectionInterface)) {
- throw new RuntimeException("Test has a bug. Expected controller with callback "
- + " implemented WaitForConnectionInterface but got " + controller);
- }
- return (WaitForConnectionInterface) callback;
- }
-
- public static void waitForConnect(MediaController2 controller, boolean expected)
- throws InterruptedException {
- getWaitForConnectionInterface(controller).waitForConnect(expected);
- }
-
- public static void waitForDisconnect(MediaController2 controller, boolean expected)
- throws InterruptedException {
- getWaitForConnectionInterface(controller).waitForDisconnect(expected);
- }
-
- TestControllerInterface onCreateController(@NonNull SessionToken2 token,
- @NonNull TestControllerCallbackInterface callback) {
- return new TestMediaController(mContext, token, new TestControllerCallback(callback));
- }
-
- public static class TestControllerCallback extends MediaController2.ControllerCallback
- implements WaitForConnectionInterface {
- public final TestControllerCallbackInterface mCallbackProxy;
- public final CountDownLatch connectLatch = new CountDownLatch(1);
- public final CountDownLatch disconnectLatch = new CountDownLatch(1);
-
- TestControllerCallback(TestControllerCallbackInterface callbackProxy) {
- mCallbackProxy = callbackProxy;
- }
-
- @CallSuper
- @Override
- public void onConnected(CommandGroup commands) {
- super.onConnected(commands);
- connectLatch.countDown();
- }
-
- @CallSuper
- @Override
- public void onDisconnected() {
- super.onDisconnected();
- disconnectLatch.countDown();
- }
-
- @Override
- public void waitForConnect(boolean expect) throws InterruptedException {
- if (expect) {
- assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } else {
- assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
- }
-
- @Override
- public void waitForDisconnect(boolean expect) throws InterruptedException {
- if (expect) {
- assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- } else {
- assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
- }
- }
-
- public class TestMediaController extends MediaController2 implements TestControllerInterface {
- private final ControllerCallback mCallback;
-
- public TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
- @NonNull ControllerCallback callback) {
- super(context, token, callback, sHandlerExecutor);
- mCallback = callback;
- }
-
- @Override
- public ControllerCallback getCallback() {
- return mCallback;
- }
- }
-}
diff --git a/android/media/MediaSessionManager_MediaSession2.java b/android/media/MediaSessionManager_MediaSession2.java
deleted file mode 100644
index 192cbc2b..00000000
--- a/android/media/MediaSessionManager_MediaSession2.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * 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.content.Context;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
-import android.media.session.MediaSessionManager;
-import android.media.session.PlaybackState;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import static android.media.TestUtils.createPlaybackState;
-import static org.junit.Assert.*;
-
-/**
- * Tests {@link MediaSessionManager} with {@link MediaSession2} specific APIs.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-@Ignore
-// TODO(jaewan): Reenable test when the media session service detects newly installed sesison
-// service app.
-public class MediaSessionManager_MediaSession2 extends MediaSession2TestBase {
- private static final String TAG = "MediaSessionManager_MediaSession2";
-
- private MediaSessionManager mManager;
- private MediaSession2 mSession;
-
- @Before
- @Override
- public void setUp() throws Exception {
- super.setUp();
- mManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
-
- // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
- // per test thread differs across the {@link MediaSession2} with the same TAG.
- final MockPlayer player = new MockPlayer(1);
- sHandler.postAndSync(() -> {
- mSession = new MediaSession2.Builder(mContext, player).setId(TAG).build();
- });
- ensureChangeInSession();
- }
-
- @After
- @Override
- public void cleanUp() throws Exception {
- super.cleanUp();
- sHandler.removeCallbacksAndMessages(null);
- sHandler.postAndSync(() -> {
- mSession.close();
- });
- }
-
- // TODO(jaewan): Make this host-side test to see per-user behavior.
- @Test
- public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
- final MockPlayer player = (MockPlayer) mSession.getPlayer();
- player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_STOPPED));
-
- MediaController2 controller = null;
- List<SessionToken2> tokens = mManager.getActiveSessionTokens();
- assertNotNull(tokens);
- for (int i = 0; i < tokens.size(); i++) {
- SessionToken2 token = tokens.get(i);
- if (mContext.getPackageName().equals(token.getPackageName())
- && TAG.equals(token.getId())) {
- assertNotNull(token.getSessionBinder());
- assertNull(controller);
- controller = createController(token);
- }
- }
- assertNotNull(controller);
-
- // Test if the found controller is correct one.
- assertEquals(PlaybackState.STATE_STOPPED, controller.getPlaybackState().getState());
- controller.play();
-
- assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- assertTrue(player.mPlayCalled);
- }
-
- /**
- * Test if server recognizes session even if session refuses the connection from server.
- *
- * @throws InterruptedException
- */
- @Test
- public void testGetSessionTokens_sessionRejected() throws InterruptedException {
- sHandler.postAndSync(() -> {
- mSession.close();
- mSession = new MediaSession2.Builder(mContext, new MockPlayer(0)).setId(TAG)
- .setSessionCallback(sHandlerExecutor, new SessionCallback() {
- @Override
- public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
- // Reject all connection request.
- return null;
- }
- }).build();
- });
- ensureChangeInSession();
-
- boolean foundSession = false;
- List<SessionToken2> tokens = mManager.getActiveSessionTokens();
- assertNotNull(tokens);
- for (int i = 0; i < tokens.size(); i++) {
- SessionToken2 token = tokens.get(i);
- if (mContext.getPackageName().equals(token.getPackageName())
- && TAG.equals(token.getId())) {
- assertFalse(foundSession);
- foundSession = true;
- }
- }
- assertTrue(foundSession);
- }
-
- @Test
- public void testGetMediaSession2Tokens_playerRemoved() throws InterruptedException {
- // Release
- sHandler.postAndSync(() -> {
- mSession.close();
- });
- ensureChangeInSession();
-
- // When the mSession's player becomes null, it should lose binder connection between server.
- // So server will forget the session.
- List<SessionToken2> tokens = mManager.getActiveSessionTokens();
- for (int i = 0; i < tokens.size(); i++) {
- SessionToken2 token = tokens.get(i);
- assertFalse(mContext.getPackageName().equals(token.getPackageName())
- && TAG.equals(token.getId()));
- }
- }
-
- @Test
- public void testGetMediaSessionService2Token() throws InterruptedException {
- boolean foundTestSessionService = false;
- boolean foundTestLibraryService = false;
- List<SessionToken2> tokens = mManager.getSessionServiceTokens();
- for (int i = 0; i < tokens.size(); i++) {
- SessionToken2 token = tokens.get(i);
- if (mContext.getPackageName().equals(token.getPackageName())
- && MockMediaSessionService2.ID.equals(token.getId())) {
- assertFalse(foundTestSessionService);
- assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
- assertNull(token.getSessionBinder());
- foundTestSessionService = true;
- } else if (mContext.getPackageName().equals(token.getPackageName())
- && MockMediaLibraryService2.ID.equals(token.getId())) {
- assertFalse(foundTestLibraryService);
- assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
- assertNull(token.getSessionBinder());
- foundTestLibraryService = true;
- }
- }
- assertTrue(foundTestSessionService);
- assertTrue(foundTestLibraryService);
- }
-
- @Test
- public void testGetAllSessionTokens() throws InterruptedException {
- boolean foundTestSession = false;
- boolean foundTestSessionService = false;
- boolean foundTestLibraryService = false;
- List<SessionToken2> tokens = mManager.getAllSessionTokens();
- for (int i = 0; i < tokens.size(); i++) {
- SessionToken2 token = tokens.get(i);
- if (!mContext.getPackageName().equals(token.getPackageName())) {
- continue;
- }
- switch (token.getId()) {
- case TAG:
- assertFalse(foundTestSession);
- foundTestSession = true;
- break;
- case MockMediaSessionService2.ID:
- assertFalse(foundTestSessionService);
- foundTestSessionService = true;
- assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
- break;
- case MockMediaLibraryService2.ID:
- assertFalse(foundTestLibraryService);
- assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
- foundTestLibraryService = true;
- break;
- default:
- fail("Unexpected session " + token + " exists in the package");
- }
- }
- assertTrue(foundTestSession);
- assertTrue(foundTestSessionService);
- assertTrue(foundTestLibraryService);
- }
-
- // Ensures if the session creation/release is notified to the server.
- private void ensureChangeInSession() throws InterruptedException {
- // TODO(jaewan): Wait by listener.
- Thread.sleep(WAIT_TIME_MS);
- }
-}
diff --git a/android/media/MediaSessionService2.java b/android/media/MediaSessionService2.java
index 19814f04..6c3a4bfd 100644
--- a/android/media/MediaSessionService2.java
+++ b/android/media/MediaSessionService2.java
@@ -23,12 +23,13 @@ import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.media.MediaSession2.ControllerInfo;
-import android.media.session.PlaybackState;
import android.media.update.ApiLoader;
import android.media.update.MediaSessionService2Provider;
+import android.media.update.MediaSessionService2Provider.MediaNotificationProvider;
import android.os.IBinder;
/**
+ * @hide
* 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
@@ -84,12 +85,13 @@ import android.os.IBinder;
* 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)}
+ * After the binding, session's {@link MediaSession2.SessionCallback#onConnect(MediaSession2, 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(PlaybackState)}
+ * When playback is started for this session service, {@link #onUpdateNotification()}
* 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.
@@ -98,21 +100,8 @@ import android.os.IBinder;
* <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
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
*/
-// TODO(jaewan): Unhide
-// TODO(jaewan): Can we clean up sessions in onDestroy() automatically instead?
-// What about currently running SessionCallback when the onDestroy() is called?
-// TODO(jaewan): Protect this with system|privilleged permission - Q.
-// TODO(jaewan): Add permission check for the service to know incoming connection request.
-// Follow-up questions: What about asking a XML for list of white/black packages for
-// allowing enumeration?
-// We can read the information even when the service is started,
-// so SessionManager.getXXXXService() can only return apps
-// TODO(jaewan): Will be the black/white listing persistent?
-// In other words, can we cache the rejection?
public abstract class MediaSessionService2 extends Service {
private final MediaSessionService2Provider mProvider;
@@ -134,7 +123,7 @@ public abstract class MediaSessionService2 extends Service {
}
MediaSessionService2Provider createProvider() {
- return ApiLoader.getProvider(this).createMediaSessionService2(this);
+ return ApiLoader.getProvider().createMediaSessionService2(this);
}
/**
@@ -169,27 +158,27 @@ public abstract class MediaSessionService2 extends Service {
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.
+ * Called when the playback state of this session is changed so notification needs update.
+ * Override this method to show or cancel 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.
*/
- // TODO(jaewan): Also add metadata
- public MediaNotification onUpdateNotification(PlaybackState2 state) {
- return mProvider.onUpdateNotification_impl(state);
+ public @Nullable MediaNotification onUpdateNotification() {
+ return mProvider.onUpdateNotification_impl();
}
/**
* Get instance of the {@link MediaSession2} that you've previously created with the
* {@link #onCreateSession} for this service.
+ * <p>
+ * This may be {@code null} before the {@link #onCreate()} is finished.
*
* @return created session
*/
- public final MediaSession2 getSession() {
+ public final @Nullable MediaSession2 getSession() {
return mProvider.getSession_impl();
}
@@ -213,35 +202,32 @@ public abstract class MediaSessionService2 extends Service {
}
/**
- * Returned by {@link #onUpdateNotification(PlaybackState)} for making session service
- * foreground service to keep playback running in the background. It's highly recommended to
- * show media style notification here.
+ * Returned by {@link #onUpdateNotification()} for making session service foreground service
+ * to keep playback running in the background. It's highly recommended to show media style
+ * notification here.
*/
- // TODO(jaewan): Should we also move this to updatable?
public static class MediaNotification {
- public final int id;
- public final Notification notification;
-
- private MediaNotification(int id, @NonNull Notification notification) {
- this.id = id;
- this.notification = notification;
- }
+ private final MediaNotificationProvider mProvider;
/**
- * Create a {@link MediaNotification}.
+ * Default constructor
*
* @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.
- * @return
*/
- public static MediaNotification create(int notificationId,
- @NonNull Notification notification) {
- if (notification == null) {
- throw new IllegalArgumentException("Notification cannot be null");
- }
- return new MediaNotification(notificationId, notification);
+ public MediaNotification(int notificationId, @NonNull Notification notification) {
+ mProvider = ApiLoader.getProvider().createMediaSessionService2MediaNotification(
+ this, notificationId, notification);
+ }
+
+ public int getNotificationId() {
+ return mProvider.getNotificationId_impl();
+ }
+
+ public @NonNull Notification getNotification() {
+ return mProvider.getNotification_impl();
}
}
}
diff --git a/android/media/MediaTimestamp.java b/android/media/MediaTimestamp.java
index 5ea6bbe8..938dd14a 100644
--- a/android/media/MediaTimestamp.java
+++ b/android/media/MediaTimestamp.java
@@ -37,6 +37,11 @@ package android.media;
public final class MediaTimestamp
{
/**
+ * An unknown media timestamp value
+ */
+ public static final MediaTimestamp TIMESTAMP_UNKNOWN = new MediaTimestamp(-1, -1, 0.0f);
+
+ /**
* Get the media time of the anchor in microseconds.
*/
public long getAnchorMediaTimeUs() {
@@ -82,4 +87,15 @@ public final class MediaTimestamp
nanoTime = 0;
clockRate = 1.0f;
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null || getClass() != obj.getClass()) return false;
+
+ final MediaTimestamp that = (MediaTimestamp) obj;
+ return (this.mediaTimeUs == that.mediaTimeUs)
+ && (this.nanoTime == that.nanoTime)
+ && (this.clockRate == that.clockRate);
+ }
}
diff --git a/android/media/MicrophoneInfo.java b/android/media/MicrophoneInfo.java
new file mode 100644
index 00000000..d6399a41
--- /dev/null
+++ b/android/media/MicrophoneInfo.java
@@ -0,0 +1,398 @@
+/*
+ * 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.annotation.NonNull;
+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;
+
+ /**
+ * Value used for when the group of the microphone is unknown.
+ */
+ public static final int GROUP_UNKNOWN = -1;
+
+ /**
+ * Value used for when the index in the group of the microphone is unknown.
+ */
+ public static final int INDEX_IN_THE_GROUP_UNKNOWN = -1;
+
+ /**
+ * Value used for when the position of the microphone is unknown.
+ */
+ public static final Coordinate3F POSITION_UNKNOWN = new Coordinate3F(
+ -Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE);
+
+ /**
+ * Value used for when the orientation of the microphone is unknown.
+ */
+ public static final Coordinate3F ORIENTATION_UNKNOWN = new Coordinate3F(0.0f, 0.0f, 0.0f);
+
+ /**
+ * Value used for when the sensitivity of the microphone is unknown.
+ */
+ public static final float SENSITIVITY_UNKNOWN = -Float.MAX_VALUE;
+
+ /**
+ * Value used for when the SPL of the microphone is unknown. This value could be used when
+ * maximum SPL or minimum SPL is unknown.
+ */
+ public static final float SPL_UNKNOWN = -Float.MAX_VALUE;
+
+ /** @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);
+ }
+
+ /**
+ * Returns The "address" string of the microphone that corresponds to the
+ * address returned by {@link AudioDeviceInfo#getAddress()}
+ * @return the address of the microphone
+ */
+ public @NonNull 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 or {@link #GROUP_UNKNOWN} if the group is unknown
+ */
+ public int getGroup() {
+ return mGroup;
+ }
+
+ /**
+ * Returns unique index for device within its group.
+ *
+ * @return the microphone's index in its group or {@link #INDEX_IN_THE_GROUP_UNKNOWN} if the
+ * index in the group is unknown
+ */
+ public int getIndexInTheGroup() {
+ return mIndexInTheGroup;
+ }
+
+ /**
+ * Returns A {@link Coordinate3F} object that represents the geometric location of microphone
+ * in meters, from bottom-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 or {@link #POSITION_UNKNOWN} if the
+ * geometric location is unknown
+ */
+ 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 or {@link #ORIENTATION_UNKNOWN} if orientation
+ * is unknown
+ */
+ 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 or {@link #SENSITIVITY_UNKNOWN} if the sensitivity
+ * is unknown
+ */
+ 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 or {@link #SPL_UNKNOWN} if maximum SPL is unknown
+ */
+ 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 or {@link #SPL_UNKNOWN} if minimum SPL is unknown
+ */
+ 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;
+ }
+
+ /**
+ * Set the channel mapping for the device.
+ * @hide
+ */
+ public void setChannelMapping(List<Pair<Integer, Integer>> channelMapping) {
+ mChannelMapping = channelMapping;
+ }
+
+ /* A class containing three float value to represent a 3D coordinate */
+ public static final 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;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Coordinate3F)) {
+ return false;
+ }
+ Coordinate3F other = (Coordinate3F) obj;
+ return this.x == other.x && this.y == other.y && this.z == other.z;
+ }
+ }
+}
diff --git a/android/media/MiniThumbFile.java b/android/media/MiniThumbFile.java
index 664308c4..98993676 100644
--- a/android/media/MiniThumbFile.java
+++ b/android/media/MiniThumbFile.java
@@ -44,13 +44,14 @@ import java.util.Hashtable;
*/
public class MiniThumbFile {
private static final String TAG = "MiniThumbFile";
- private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
+ private static final int MINI_THUMB_DATA_FILE_VERSION = 4;
public static final int BYTES_PER_MINTHUMB = 10000;
private static final int HEADER_SIZE = 1 + 8 + 4;
private Uri mUri;
private RandomAccessFile mMiniThumbFile;
private FileChannel mChannel;
private ByteBuffer mBuffer;
+ private ByteBuffer mEmptyBuffer;
private static final Hashtable<String, MiniThumbFile> sThumbFiles =
new Hashtable<String, MiniThumbFile>();
@@ -127,9 +128,10 @@ public class MiniThumbFile {
return mMiniThumbFile;
}
- public MiniThumbFile(Uri uri) {
+ private MiniThumbFile(Uri uri) {
mUri = uri;
mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
+ mEmptyBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
}
public synchronized void deactivate() {
@@ -184,6 +186,54 @@ public class MiniThumbFile {
return 0;
}
+ public synchronized void eraseMiniThumb(long id) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r != null) {
+ long pos = id * BYTES_PER_MINTHUMB;
+ FileLock lock = null;
+ try {
+ mBuffer.clear();
+ mBuffer.limit(1 + 8);
+
+ lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);
+ // check that we can read the following 9 bytes
+ // (1 for the "status" and 8 for the long)
+ if (mChannel.read(mBuffer, pos) == 9) {
+ mBuffer.position(0);
+ if (mBuffer.get() == 1) {
+ long currentMagic = mBuffer.getLong();
+ if (currentMagic == 0) {
+ // there is no thumbnail stored here
+ Log.i(TAG, "no thumbnail for id " + id);
+ return;
+ }
+ // zero out the thumbnail slot
+ // Log.v(TAG, "clearing slot " + id + ", magic " + currentMagic
+ // + " at offset " + pos);
+ mChannel.write(mEmptyBuffer, pos);
+ }
+ } else {
+ // Log.v(TAG, "No slot");
+ }
+ } catch (IOException ex) {
+ Log.v(TAG, "Got exception checking file magic: ", ex);
+ } catch (RuntimeException ex) {
+ // Other NIO related exception like disk full, read only channel..etc
+ Log.e(TAG, "Got exception when reading magic, id = " + id +
+ ", disk full or mount read-only? " + ex.getClass());
+ } finally {
+ try {
+ if (lock != null) lock.release();
+ }
+ catch (IOException ex) {
+ // ignore it.
+ }
+ }
+ } else {
+ // Log.v(TAG, "No data file");
+ }
+ }
+
public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic)
throws IOException {
RandomAccessFile r = miniThumbDataFile();
diff --git a/android/media/MockMediaLibraryService2.java b/android/media/MockMediaLibraryService2.java
deleted file mode 100644
index 14cf2577..00000000
--- a/android/media/MockMediaLibraryService2.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
-* 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 static junit.framework.Assert.fail;
-
-import android.content.Context;
-import android.media.MediaSession2.CommandGroup;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.TestUtils.SyncHandler;
-import android.os.Bundle;
-import android.os.Process;
-
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * Mock implementation of {@link MediaLibraryService2} for testing.
- */
-public class MockMediaLibraryService2 extends MediaLibraryService2 {
- // Keep in sync with the AndroidManifest.xml
- public static final String ID = "TestLibrary";
-
- public static final String ROOT_ID = "rootId";
- public static final Bundle EXTRA = new Bundle();
- static {
- EXTRA.putString(ROOT_ID, ROOT_ID);
- }
- @GuardedBy("MockMediaLibraryService2.class")
- private static SessionToken2 sToken;
-
- private MediaLibrarySession mSession;
-
- @Override
- public MediaLibrarySession onCreateSession(String sessionId) {
- final MockPlayer player = new MockPlayer(1);
- final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
- try {
- handler.postAndSync(() -> {
- TestLibrarySessionCallback callback = new TestLibrarySessionCallback();
- mSession = new MediaLibrarySessionBuilder(MockMediaLibraryService2.this,
- player, (runnable) -> handler.post(runnable), callback)
- .setId(sessionId).build();
- });
- } catch (InterruptedException e) {
- fail(e.toString());
- }
- return mSession;
- }
-
- @Override
- public void onDestroy() {
- TestServiceRegistry.getInstance().cleanUp();
- super.onDestroy();
- }
-
- public static SessionToken2 getToken(Context context) {
- synchronized (MockMediaLibraryService2.class) {
- if (sToken == null) {
- sToken = new SessionToken2(SessionToken2.TYPE_LIBRARY_SERVICE,
- context.getPackageName(), ID,
- MockMediaLibraryService2.class.getName(), null);
- }
- return sToken;
- }
- }
-
- private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
- @Override
- public CommandGroup onConnect(ControllerInfo controller) {
- if (Process.myUid() != controller.getUid()) {
- // It's system app wants to listen changes. Ignore.
- return super.onConnect(controller);
- }
- TestServiceRegistry.getInstance().setServiceInstance(
- MockMediaLibraryService2.this, controller);
- return super.onConnect(controller);
- }
-
- @Override
- public BrowserRoot onGetRoot(ControllerInfo controller, Bundle rootHints) {
- return new BrowserRoot(ROOT_ID, EXTRA);
- }
- }
-} \ No newline at end of file
diff --git a/android/media/MockMediaSessionService2.java b/android/media/MockMediaSessionService2.java
deleted file mode 100644
index b0581170..00000000
--- a/android/media/MockMediaSessionService2.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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 static junit.framework.Assert.fail;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
-import android.media.TestUtils.SyncHandler;
-import android.media.session.PlaybackState;
-import android.os.Process;
-
-/**
- * Mock implementation of {@link android.media.MediaSessionService2} for testing.
- */
-public class MockMediaSessionService2 extends MediaSessionService2 {
- // Keep in sync with the AndroidManifest.xml
- public static final String ID = "TestSession";
-
- private static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
- private static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
-
- private NotificationChannel mDefaultNotificationChannel;
- private MediaSession2 mSession;
- private NotificationManager mNotificationManager;
-
- @Override
- public MediaSession2 onCreateSession(String sessionId) {
- final MockPlayer player = new MockPlayer(1);
- final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
- try {
- handler.postAndSync(() -> {
- mSession = new MediaSession2.Builder(MockMediaSessionService2.this, player)
- .setId(sessionId).setSessionCallback((runnable)->handler.post(runnable),
- new MySessionCallback()).build();
- });
- } catch (InterruptedException e) {
- fail(e.toString());
- }
- return mSession;
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- }
-
- @Override
- public void onDestroy() {
- TestServiceRegistry.getInstance().cleanUp();
- super.onDestroy();
- }
-
- @Override
- public MediaNotification onUpdateNotification(PlaybackState2 state) {
- if (mDefaultNotificationChannel == null) {
- mDefaultNotificationChannel = new NotificationChannel(
- DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
- DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
- NotificationManager.IMPORTANCE_DEFAULT);
- mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
- }
- Notification notification = new Notification.Builder(
- this, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
- .setContentTitle(getPackageName())
- .setContentText("Playback state: " + state.getState())
- .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
- return MediaNotification.create(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
- }
-
- private class MySessionCallback extends SessionCallback {
- @Override
- public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
- if (Process.myUid() != controller.getUid()) {
- // It's system app wants to listen changes. Ignore.
- return super.onConnect(controller);
- }
- TestServiceRegistry.getInstance().setServiceInstance(
- MockMediaSessionService2.this, controller);
- return super.onConnect(controller);
- }
- }
-}
diff --git a/android/media/MockPlayer.java b/android/media/MockPlayer.java
deleted file mode 100644
index fd693092..00000000
--- a/android/media/MockPlayer.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * 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.media.MediaSession2.PlaylistParam;
-import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
-
-/**
- * A mock implementation of {@link MediaPlayerBase} for testing.
- */
-public class MockPlayer extends MediaPlayerBase {
- public final CountDownLatch mCountDownLatch;
-
- public boolean mPlayCalled;
- public boolean mPauseCalled;
- public boolean mStopCalled;
- public boolean mSkipToPreviousCalled;
- public boolean mSkipToNextCalled;
- public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
- private PlaybackState2 mLastPlaybackState;
-
- public MockPlayer(int count) {
- mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
- }
-
- @Override
- public void play() {
- mPlayCalled = true;
- if (mCountDownLatch != null) {
- mCountDownLatch.countDown();
- }
- }
-
- @Override
- public void pause() {
- mPauseCalled = true;
- if (mCountDownLatch != null) {
- mCountDownLatch.countDown();
- }
- }
-
- @Override
- public void stop() {
- mStopCalled = true;
- if (mCountDownLatch != null) {
- mCountDownLatch.countDown();
- }
- }
-
- @Override
- public void skipToPrevious() {
- mSkipToPreviousCalled = true;
- if (mCountDownLatch != null) {
- mCountDownLatch.countDown();
- }
- }
-
- @Override
- public void skipToNext() {
- mSkipToNextCalled = true;
- if (mCountDownLatch != null) {
- mCountDownLatch.countDown();
- }
- }
-
-
-
- @Nullable
- @Override
- public PlaybackState2 getPlaybackState() {
- return mLastPlaybackState;
- }
-
- @Override
- public void addPlaybackListener(@NonNull Executor executor,
- @NonNull PlaybackListener listener) {
- mListeners.add(new PlaybackListenerHolder(executor, listener));
- }
-
- @Override
- public void removePlaybackListener(@NonNull PlaybackListener listener) {
- int index = PlaybackListenerHolder.indexOf(mListeners, listener);
- if (index >= 0) {
- mListeners.remove(index);
- }
- }
-
- public void notifyPlaybackState(final PlaybackState2 state) {
- mLastPlaybackState = state;
- for (int i = 0; i < mListeners.size(); i++) {
- mListeners.get(i).postPlaybackChange(state);
- }
- }
-
- // No-op. Should be added for test later.
- @Override
- public void prepare() {
- }
-
- @Override
- public void seekTo(long pos) {
- }
-
- @Override
- public void fastFoward() {
- }
-
- @Override
- public void rewind() {
- }
-
- @Override
- public AudioAttributes getAudioAttributes() {
- return null;
- }
-
- @Override
- public void setPlaylist(List<MediaItem2> item, PlaylistParam param) {
- }
-
- @Override
- public void setCurrentPlaylistItem(int index) {
- }
-}
diff --git a/android/media/PlaybackListenerHolder.java b/android/media/PlaybackListenerHolder.java
deleted file mode 100644
index 4e19d4de..00000000
--- a/android/media/PlaybackListenerHolder.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.media.MediaPlayerBase.PlaybackListener;
-import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.os.Message;
-import android.support.annotation.NonNull;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Holds {@link PlaybackListener} with the {@link Handler}.
- */
-public class PlaybackListenerHolder {
- public final Executor executor;
- public final PlaybackListener listener;
-
- public PlaybackListenerHolder(Executor executor, @NonNull PlaybackListener listener) {
- this.executor = executor;
- this.listener = listener;
- }
-
- public void postPlaybackChange(final PlaybackState2 state) {
- executor.execute(() -> listener.onPlaybackChanged(state));
- }
-
- /**
- * Returns {@code true} if the given list contains a {@link PlaybackListenerHolder} that holds
- * the given listener.
- *
- * @param list list to check
- * @param listener listener to check
- * @return {@code true} if the given list contains listener. {@code false} otherwise.
- */
- public static <Holder extends PlaybackListenerHolder> boolean contains(
- @NonNull List<Holder> list, PlaybackListener listener) {
- return indexOf(list, listener) >= 0;
- }
-
- /**
- * Returns the index of the {@link PlaybackListenerHolder} that contains the given listener.
- *
- * @param list list to check
- * @param listener listener to check
- * @return {@code index} of item if the given list contains listener. {@code -1} otherwise.
- */
- public static <Holder extends PlaybackListenerHolder> int indexOf(
- @NonNull List<Holder> list, PlaybackListener listener) {
- for (int i = 0; i < list.size(); i++) {
- if (list.get(i).listener == listener) {
- return i;
- }
- }
- return -1;
- }
-}
diff --git a/android/media/PlaybackState2.java b/android/media/PlaybackState2.java
deleted file mode 100644
index 46d6f45a..00000000
--- a/android/media/PlaybackState2.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * 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.os.Bundle;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Playback state for a {@link MediaPlayerBase}, to be shared between {@link MediaSession2} and
- * {@link MediaController2}. This includes a playback state {@link #STATE_PLAYING},
- * the current playback position and extra.
- * @hide
- */
-// TODO(jaewan): Move to updatable
-public final class PlaybackState2 {
- private static final String TAG = "PlaybackState2";
-
- private static final String KEY_STATE = "android.media.playbackstate2.state";
-
- // TODO(jaewan): Replace states from MediaPlayer2
- /**
- * @hide
- */
- @IntDef({STATE_NONE, STATE_STOPPED, STATE_PREPARED, STATE_PAUSED, STATE_PLAYING,
- STATE_FINISH, 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 prepared
- */
- public final static int STATE_PREPARED = 2;
-
- /**
- * State indicating this item is currently paused.
- */
- public final static int STATE_PAUSED = 3;
-
- /**
- * State indicating this item is currently playing.
- */
- public final static int STATE_PLAYING = 4;
-
- /**
- * State indicating the playback reaches the end of the item.
- */
- public final static int STATE_FINISH = 5;
-
- /**
- * State indicating this item is currently buffering and will begin playing
- * when enough data has buffered.
- */
- public final static int STATE_BUFFERING = 6;
-
- /**
- * 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 = 7;
-
- /**
- * Use this value for the position to indicate the position is not known.
- */
- public final static long PLAYBACK_POSITION_UNKNOWN = -1;
-
- private final int mState;
- private final long mPosition;
- private final long mBufferedPosition;
- private final float mSpeed;
- private final CharSequence mErrorMessage;
- private final long mUpdateTime;
- private final long mActiveItemId;
-
- public PlaybackState2(int state, long position, long updateTime, float speed,
- long bufferedPosition, long activeItemId, CharSequence error) {
- mState = state;
- mPosition = position;
- mSpeed = speed;
- mUpdateTime = updateTime;
- mBufferedPosition = bufferedPosition;
- mActiveItemId = activeItemId;
- mErrorMessage = error;
- }
-
- @Override
- public String toString() {
- StringBuilder bob = new StringBuilder("PlaybackState {");
- bob.append("state=").append(mState);
- bob.append(", position=").append(mPosition);
- bob.append(", buffered position=").append(mBufferedPosition);
- bob.append(", speed=").append(mSpeed);
- bob.append(", updated=").append(mUpdateTime);
- bob.append(", active item id=").append(mActiveItemId);
- bob.append(", error=").append(mErrorMessage);
- bob.append("}");
- return bob.toString();
- }
-
- /**
- * 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_PLAYING}</li>
- * <li> {@link PlaybackState2#STATE_PAUSED}</li>
- * <li> {@link PlaybackState2#STATE_BUFFERING}</li>
- * <li> {@link PlaybackState2#STATE_ERROR}</li>
- * </ul>
- */
- @State
- public int getState() {
- return mState;
- }
-
- /**
- * Get the current playback position in ms.
- */
- public long getPosition() {
- return mPosition;
- }
-
- /**
- * 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 mBufferedPosition;
- }
-
- /**
- * 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 mSpeed;
- }
-
- /**
- * Get a user readable error message. This should be set when the state is
- * {@link PlaybackState2#STATE_ERROR}.
- */
- public CharSequence getErrorMessage() {
- return mErrorMessage;
- }
-
- /**
- * 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 mUpdateTime;
- }
-
- /**
- * 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 mActiveItemId;
- }
-
- /**
- * @return Bundle object for this to share between processes.
- */
- public Bundle toBundle() {
- // TODO(jaewan): Include other variables.
- Bundle bundle = new Bundle();
- bundle.putInt(KEY_STATE, mState);
- return bundle;
- }
-
- /**
- * @param bundle input
- * @return
- */
- public static PlaybackState2 fromBundle(Bundle bundle) {
- // TODO(jaewan): Include other variables.
- final int state = bundle.getInt(KEY_STATE);
- return new PlaybackState2(state, 0, 0, 0, 0, 0, null);
- }
-} \ No newline at end of file
diff --git a/android/media/PlayerBase.java b/android/media/PlayerBase.java
index 09449a18..80049ba5 100644
--- a/android/media/PlayerBase.java
+++ b/android/media/PlayerBase.java
@@ -47,10 +47,10 @@ import java.util.Objects;
public abstract class PlayerBase {
private static final String TAG = "PlayerBase";
- private static final boolean DEBUG = false;
- private static IAudioService sService; //lazy initialization, use getService()
/** Debug app ops */
private static final boolean DEBUG_APP_OPS = false;
+ private static final boolean DEBUG = DEBUG_APP_OPS || false;
+ private static IAudioService sService; //lazy initialization, use getService()
// parameters of the player that affect AppOps
protected AudioAttributes mAttributes;
@@ -102,6 +102,7 @@ public abstract class PlayerBase {
mAppOps.startWatchingMode(AppOpsManager.OP_PLAY_AUDIO,
ActivityThread.currentPackageName(), mAppOpsCallback);
} catch (RemoteException e) {
+ Log.e(TAG, "Error registering appOps callback", e);
mHasAppOpsPlayAudio = false;
}
try {
diff --git a/android/media/Rating2.java b/android/media/Rating2.java
index 67e5e728..92131901 100644
--- a/android/media/Rating2.java
+++ b/android/media/Rating2.java
@@ -16,7 +16,11 @@
package android.media;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.IntDef;
+import android.media.update.ApiLoader;
+import android.media.update.Rating2Provider;
import android.os.Bundle;
import android.util.Log;
@@ -24,21 +28,17 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
+ * @hide
* 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
*/
-// TODO(jaewan): Move this to updatable
+// New version of Rating with following change
+// - Don't implement Parcelable for updatable support.
public final class Rating2 {
- private static final String TAG = "Rating2";
-
- private static final String KEY_STYLE = "android.media.rating2.style";
- private static final String KEY_VALUE = "android.media.rating2.value";
-
/**
* @hide
*/
@@ -92,31 +92,45 @@ public final class Rating2 {
*/
public final static int RATING_PERCENTAGE = 6;
- private final static float RATING_NOT_RATED = -1.0f;
+ private final Rating2Provider mProvider;
+
+ /**
+ * @hide
+ */
+ public Rating2(@NonNull Rating2Provider provider) {
+ mProvider = provider;
+ }
- private final int mRatingStyle;
+ @Override
+ public String toString() {
+ return mProvider.toString_impl();
+ }
- private final float mRatingValue;
+ /**
+ * @hide
+ */
+ public Rating2Provider getProvider() {
+ return mProvider;
+ }
- private Rating2(@Style int ratingStyle, float rating) {
- mRatingStyle = ratingStyle;
- mRatingValue = rating;
+ @Override
+ public boolean equals(Object obj) {
+ return mProvider.equals_impl(obj);
}
@Override
- public String toString() {
- return "Rating2:style=" + mRatingStyle + " rating="
- + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+ public int hashCode() {
+ return mProvider.hashCode_impl();
}
/**
* Create an instance from bundle object, previoulsy created by {@link #toBundle()}
*
* @param bundle bundle
- * @return new Rating2 instance
+ * @return new Rating2 instance or {@code null} for error
*/
- public static Rating2 fromBundle(Bundle bundle) {
- return new Rating2(bundle.getInt(KEY_STYLE), bundle.getFloat(KEY_VALUE));
+ public static Rating2 fromBundle(@Nullable Bundle bundle) {
+ return ApiLoader.getProvider().fromBundle_Rating2(bundle);
}
/**
@@ -124,10 +138,7 @@ public final class Rating2 {
* @return bundle of this object
*/
public Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putInt(KEY_STYLE, mRatingStyle);
- bundle.putFloat(KEY_VALUE, mRatingValue);
- return bundle;
+ return mProvider.toBundle_impl();
}
/**
@@ -139,18 +150,8 @@ public final class Rating2 {
* or {@link #RATING_PERCENTAGE}.
* @return null if an invalid rating style is passed, a new Rating2 instance otherwise.
*/
- public static Rating2 newUnratedRating(@Style int ratingStyle) {
- switch(ratingStyle) {
- case RATING_HEART:
- case RATING_THUMB_UP_DOWN:
- case RATING_3_STARS:
- case RATING_4_STARS:
- case RATING_5_STARS:
- case RATING_PERCENTAGE:
- return new Rating2(ratingStyle, RATING_NOT_RATED);
- default:
- return null;
- }
+ public static @Nullable Rating2 newUnratedRating(@Style int ratingStyle) {
+ return ApiLoader.getProvider().newUnratedRating_Rating2(ratingStyle);
}
/**
@@ -160,8 +161,8 @@ public final class Rating2 {
* @param hasHeart true for a "heart selected" rating, false for "heart unselected".
* @return a new Rating2 instance.
*/
- public static Rating2 newHeartRating(boolean hasHeart) {
- return new Rating2(RATING_HEART, hasHeart ? 1.0f : 0.0f);
+ public static @Nullable Rating2 newHeartRating(boolean hasHeart) {
+ return ApiLoader.getProvider().newHeartRating_Rating2(hasHeart);
}
/**
@@ -171,8 +172,8 @@ public final class Rating2 {
* @param thumbIsUp true for a "thumb up" rating, false for "thumb down".
* @return a new Rating2 instance.
*/
- public static Rating2 newThumbRating(boolean thumbIsUp) {
- return new Rating2(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f);
+ public static @Nullable Rating2 newThumbRating(boolean thumbIsUp) {
+ return ApiLoader.getProvider().newThumbRating_Rating2(thumbIsUp);
}
/**
@@ -187,27 +188,9 @@ public final class Rating2 {
* @return null if the rating style is invalid, or the rating is out of range,
* a new Rating2 instance otherwise.
*/
- public static Rating2 newStarRating(@StarStyle int starRatingStyle, float starRating) {
- float maxRating = -1.0f;
- switch(starRatingStyle) {
- case RATING_3_STARS:
- maxRating = 3.0f;
- break;
- case RATING_4_STARS:
- maxRating = 4.0f;
- break;
- case RATING_5_STARS:
- maxRating = 5.0f;
- break;
- default:
- Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating");
- return null;
- }
- if ((starRating < 0.0f) || (starRating > maxRating)) {
- Log.e(TAG, "Trying to set out of range star-based rating");
- return null;
- }
- return new Rating2(starRatingStyle, starRating);
+ public static @Nullable Rating2 newStarRating(
+ @StarStyle int starRatingStyle, float starRating) {
+ return ApiLoader.getProvider().newStarRating_Rating2(starRatingStyle, starRating);
}
/**
@@ -217,13 +200,8 @@ public final class Rating2 {
* @param percent the value of the rating
* @return null if the rating is out of range, a new Rating2 instance otherwise.
*/
- public static Rating2 newPercentageRating(float percent) {
- if ((percent < 0.0f) || (percent > 100.0f)) {
- Log.e(TAG, "Invalid percentage-based rating value");
- return null;
- } else {
- return new Rating2(RATING_PERCENTAGE, percent);
- }
+ public static @Nullable Rating2 newPercentageRating(float percent) {
+ return ApiLoader.getProvider().newPercentageRating_Rating2(percent);
}
/**
@@ -231,7 +209,7 @@ public final class Rating2 {
* @return true if the instance was not created with {@link #newUnratedRating(int)}.
*/
public boolean isRated() {
- return mRatingValue >= 0.0f;
+ return mProvider.isRated_impl();
}
/**
@@ -240,9 +218,8 @@ public final class Rating2 {
* {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
* or {@link #RATING_PERCENTAGE}.
*/
- @Style
- public int getRatingStyle() {
- return mRatingStyle;
+ public @Style int getRatingStyle() {
+ return mProvider.getRatingStyle_impl();
}
/**
@@ -251,11 +228,7 @@ public final class Rating2 {
* if the rating style is not {@link #RATING_HEART} or if it is unrated.
*/
public boolean hasHeart() {
- if (mRatingStyle != RATING_HEART) {
- return false;
- } else {
- return (mRatingValue == 1.0f);
- }
+ return mProvider.hasHeart_impl();
}
/**
@@ -264,11 +237,7 @@ public final class Rating2 {
* if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated.
*/
public boolean isThumbUp() {
- if (mRatingStyle != RATING_THUMB_UP_DOWN) {
- return false;
- } else {
- return (mRatingValue == 1.0f);
- }
+ return mProvider.isThumbUp_impl();
}
/**
@@ -277,16 +246,7 @@ public final class Rating2 {
* not star-based, or if it is unrated.
*/
public float getStarRating() {
- switch (mRatingStyle) {
- case RATING_3_STARS:
- case RATING_4_STARS:
- case RATING_5_STARS:
- if (isRated()) {
- return mRatingValue;
- }
- default:
- return -1.0f;
- }
+ return mProvider.getStarRating_impl();
}
/**
@@ -295,10 +255,6 @@ public final class Rating2 {
* not percentage-based, or if it is unrated.
*/
public float getPercentRating() {
- if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) {
- return -1.0f;
- } else {
- return mRatingValue;
- }
+ return mProvider.getPercentRating_impl();
}
}
diff --git a/android/media/Ringtone.java b/android/media/Ringtone.java
index 209ec42d..c0468dc9 100644
--- a/android/media/Ringtone.java
+++ b/android/media/Ringtone.java
@@ -40,7 +40,7 @@ import java.util.ArrayList;
* <p>
* For ways of retrieving {@link Ringtone} objects or to show a ringtone
* picker, see {@link RingtoneManager}.
- *
+ *
* @see RingtoneManager
*/
public class Ringtone {
@@ -97,7 +97,7 @@ public class Ringtone {
/**
* Sets the stream type where this ringtone will be played.
- *
+ *
* @param streamType The stream, see {@link AudioManager}.
* @deprecated use {@link #setAudioAttributes(AudioAttributes)}
*/
@@ -111,7 +111,7 @@ public class Ringtone {
/**
* Gets the stream type where this ringtone will be played.
- *
+ *
* @return The stream type, see {@link AudioManager}.
* @deprecated use of stream types is deprecated, see
* {@link #setAudioAttributes(AudioAttributes)}
@@ -146,9 +146,8 @@ public class Ringtone {
}
/**
- * @hide
* Sets the player to be looping or non-looping.
- * @param looping whether to loop or not
+ * @param looping whether to loop or not.
*/
public void setLooping(boolean looping) {
synchronized (mPlaybackSettingsLock) {
@@ -158,7 +157,16 @@ public class Ringtone {
}
/**
- * @hide
+ * Returns whether the looping mode was enabled on this player.
+ * @return true if this player loops when playing.
+ */
+ public boolean isLooping() {
+ synchronized (mPlaybackSettingsLock) {
+ return mIsLooping;
+ }
+ }
+
+ /**
* Sets the volume on this player.
* @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
* corresponds to no attenuation being applied.
@@ -173,6 +181,16 @@ public class Ringtone {
}
/**
+ * Returns the volume scalar set on this player.
+ * @return a value between 0.0f and 1.0f.
+ */
+ public float getVolume() {
+ synchronized (mPlaybackSettingsLock) {
+ return mVolume;
+ }
+ }
+
+ /**
* Must be called synchronized on mPlaybackSettingsLock
*/
private void applyPlaybackProperties_sync() {
@@ -194,8 +212,8 @@ public class Ringtone {
/**
* Returns a human-presentable title for ringtone. Looks in media
* content provider. If not in either, uses the filename
- *
- * @param context A context used for querying.
+ *
+ * @param context A context used for querying.
*/
public String getTitle(Context context) {
if (mTitle != null) return mTitle;
@@ -265,12 +283,11 @@ public class Ringtone {
if (title == null) {
title = context.getString(com.android.internal.R.string.ringtone_unknown);
-
if (title == null) {
title = "";
}
}
-
+
return title;
}
@@ -395,7 +412,7 @@ public class Ringtone {
/**
* Whether this ringtone is currently playing.
- *
+ *
* @return True if playing, false otherwise.
*/
public boolean isPlaying() {
diff --git a/android/media/RingtoneManager.java b/android/media/RingtoneManager.java
index 3eb9d529..fefa1ede 100644
--- a/android/media/RingtoneManager.java
+++ b/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/android/media/SessionCommand2.java b/android/media/SessionCommand2.java
new file mode 100644
index 00000000..fe86a3ae
--- /dev/null
+++ b/android/media/SessionCommand2.java
@@ -0,0 +1,336 @@
+/*
+ * 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.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSession2Provider;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.List;
+
+/**
+ * @hide
+ * 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}.
+ */
+public final class SessionCommand2 {
+ /**
+ * Command code for the custom command which can be defined by string action in the
+ * {@link SessionCommand2}.
+ */
+ 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(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ 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(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ 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(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_STOP = 3;
+
+ /**
+ * Command code for {@link MediaController2#skipToNextItem()}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the {@link SessionCallback#onCommandRequest(
+ * MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM = 4;
+
+ /**
+ * Command code for {@link MediaController2#skipToPreviousItem()}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the {@link SessionCallback#onCommandRequest(
+ * MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_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(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
+
+ /**
+ * Command code for {@link MediaController2#fastForward()}.
+ */
+ public static final int COMMAND_CODE_SESSION_FAST_FORWARD = 7;
+
+ /**
+ * Command code for {@link MediaController2#rewind()}.
+ */
+ public static final int COMMAND_CODE_SESSION_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(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
+
+ /**
+ * Command code for both {@link MediaController2#setVolumeTo(int, int)}.
+ * <p>
+ * Command would set the device volume or send to the volume provider directly if the session
+ * doesn't reject the request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_SET_VOLUME = 10;
+
+ /**
+ * Command code for both {@link MediaController2#adjustVolume(int, int)}.
+ * <p>
+ * Command would adjust the device volume or send to the volume provider directly if the session
+ * doesn't reject the request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_ADJUST_VOLUME = 11;
+
+ /**
+ * Command code for {@link MediaController2#skipToPlaylistItem(MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM = 12;
+
+ /**
+ * Command code for {@link MediaController2#setShuffleMode(int)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE = 13;
+
+ /**
+ * Command code for {@link MediaController2#setRepeatMode(int)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE = 14;
+
+ /**
+ * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_ADD_ITEM = 15;
+
+ /**
+ * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_REMOVE_ITEM = 16;
+
+ /**
+ * Command code for {@link MediaController2#replacePlaylistItem(int, MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_REPLACE_ITEM = 17;
+
+ /**
+ * Command code for {@link MediaController2#getPlaylist()}. This will expose metadata
+ * information to the controller.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_GET_LIST = 18;
+
+ /**
+ * Command code for {@link MediaController2#setPlaylist(List, MediaMetadata2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SET_LIST = 19;
+
+ /**
+ * Command code for {@link MediaController2#getPlaylistMetadata()}. This will expose
+ * metadata information to the controller.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_GET_LIST_METADATA = 20;
+
+ /**
+ * Command code for {@link MediaController2#updatePlaylistMetadata(MediaMetadata2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SET_LIST_METADATA = 21;
+
+ /**
+ * Command code for {@link MediaController2#playFromMediaId(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 22;
+
+ /**
+ * Command code for {@link MediaController2#playFromUri(Uri, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PLAY_FROM_URI = 23;
+
+ /**
+ * Command code for {@link MediaController2#playFromSearch(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PLAY_FROM_SEARCH = 24;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID = 25;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PREPARE_FROM_URI = 26;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromSearch(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH = 27;
+
+ /**
+ * Command code for {@link MediaController2#setRating(String, Rating2)}.
+ */
+ public static final int COMMAND_CODE_SESSION_SET_RATING = 28;
+
+ // TODO(jaewan): Add javadoc
+ public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 29;
+ public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30;
+ public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31;
+ public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32;
+ public static final int COMMAND_CODE_LIBRARY_SEARCH = 33;
+ public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34;
+ public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35;
+
+ // TODO(jaewan): Rename and move provider
+ private final MediaSession2Provider.CommandProvider mProvider;
+
+ public SessionCommand2(int commandCode) {
+ mProvider = ApiLoader.getProvider().createMediaSession2Command(
+ this, commandCode, null, null);
+ }
+
+ public SessionCommand2(@NonNull String action, @Nullable Bundle extras) {
+ if (action == null) {
+ throw new IllegalArgumentException("action shouldn't be null");
+ }
+ mProvider = ApiLoader.getProvider().createMediaSession2Command(
+ this, COMMAND_CODE_CUSTOM, action, extras);
+ }
+
+ /**
+ * @hide
+ */
+ public MediaSession2Provider.CommandProvider getProvider() {
+ return mProvider;
+ }
+
+ public int getCommandCode() {
+ return mProvider.getCommandCode_impl();
+ }
+
+ public @Nullable String getCustomCommand() {
+ return mProvider.getCustomCommand_impl();
+ }
+
+ public @Nullable Bundle getExtras() {
+ return mProvider.getExtras_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 SessionCommand2)) {
+ return false;
+ }
+ return mProvider.equals_impl(((SessionCommand2) obj).mProvider);
+ }
+
+ @Override
+ public int hashCode() {
+ return mProvider.hashCode_impl();
+ }
+
+ /**
+ * @return a new Command instance from the Bundle
+ * @hide
+ */
+ public static SessionCommand2 fromBundle(@NonNull Bundle command) {
+ return ApiLoader.getProvider().fromBundle_MediaSession2Command(command);
+ }
+}
diff --git a/android/media/SessionCommandGroup2.java b/android/media/SessionCommandGroup2.java
new file mode 100644
index 00000000..399765e6
--- /dev/null
+++ b/android/media/SessionCommandGroup2.java
@@ -0,0 +1,106 @@
+/*
+ * 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.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSession2Provider;
+import android.os.Bundle;
+
+import java.util.Set;
+
+/**
+ * @hide
+ * Represent set of {@link SessionCommand2}.
+ */
+public final class SessionCommandGroup2 {
+ // TODO(jaewan): Rename and move provider
+ private final MediaSession2Provider.CommandGroupProvider mProvider;
+
+ public SessionCommandGroup2() {
+ mProvider = ApiLoader.getProvider().createMediaSession2CommandGroup(this, null);
+ }
+
+ public SessionCommandGroup2(@Nullable SessionCommandGroup2 others) {
+ mProvider = ApiLoader.getProvider().createMediaSession2CommandGroup(this, others);
+ }
+
+ /**
+ * @hide
+ */
+ public SessionCommandGroup2(@NonNull MediaSession2Provider.CommandGroupProvider provider) {
+ mProvider = provider;
+ }
+
+ public void addCommand(@NonNull SessionCommand2 command) {
+ mProvider.addCommand_impl(command);
+ }
+
+ public void addCommand(int commandCode) {
+ // TODO(jaewna): Implement
+ }
+
+ public void addAllPredefinedCommands() {
+ mProvider.addAllPredefinedCommands_impl();
+ }
+
+ public void removeCommand(@NonNull SessionCommand2 command) {
+ mProvider.removeCommand_impl(command);
+ }
+
+ public void removeCommand(int commandCode) {
+ // TODO(jaewan): Implement.
+ }
+
+ public boolean hasCommand(@NonNull SessionCommand2 command) {
+ return mProvider.hasCommand_impl(command);
+ }
+
+ public boolean hasCommand(int code) {
+ return mProvider.hasCommand_impl(code);
+ }
+
+ public @NonNull
+ Set<SessionCommand2> getCommands() {
+ return mProvider.getCommands_impl();
+ }
+
+ /**
+ * @hide
+ */
+ public @NonNull MediaSession2Provider.CommandGroupProvider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * @return new bundle from the CommandGroup
+ * @hide
+ */
+ public @NonNull Bundle toBundle() {
+ return mProvider.toBundle_impl();
+ }
+
+ /**
+ * @return new instance of CommandGroup from the bundle
+ * @hide
+ */
+ public static @Nullable SessionCommandGroup2 fromBundle(Bundle commands) {
+ return ApiLoader.getProvider().fromBundle_MediaSession2CommandGroup(commands);
+ }
+}
diff --git a/android/media/SessionToken2.java b/android/media/SessionToken2.java
index 697a5a87..bf2d4459 100644
--- a/android/media/SessionToken2.java
+++ b/android/media/SessionToken2.java
@@ -18,16 +18,17 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
-import android.annotation.Nullable;
+import android.content.Context;
import android.media.session.MediaSessionManager;
+import android.media.update.ApiLoader;
+import android.media.update.SessionToken2Provider;
import android.os.Bundle;
-import android.os.IBinder;
-import android.text.TextUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
+ * @hide
* Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
* If it's representing a session service, it may not be ongoing.
* <p>
@@ -35,11 +36,11 @@ import java.lang.annotation.RetentionPolicy;
* {@link MediaController2} to communicate with the session.
* <p>
* It can be also obtained by {@link MediaSessionManager}.
- * @hide
*/
-// TODO(jaewan): Unhide. SessionToken2?
-// TODO(jaewan): Move Token to updatable!
-// TODO(jaewan): Find better name for this (SessionToken or Session2Token)
+// New version of MediaSession.Token for following reasons
+// - Stop implementing Parcelable for updatable support
+// - Represent session and library service (formerly browser service) in one class.
+// Previously MediaSession.Token was for session and ComponentName was for service.
public final class SessionToken2 {
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
@@ -50,176 +51,114 @@ public final class SessionToken2 {
public static final int TYPE_SESSION_SERVICE = 1;
public static final int TYPE_LIBRARY_SERVICE = 2;
- private static final String KEY_TYPE = "android.media.token.type";
- private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
- private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
- private static final String KEY_ID = "android.media.token.id";
- private static final String KEY_SESSION_BINDER = "android.media.token.session_binder";
+ private final SessionToken2Provider mProvider;
- private final @TokenType int mType;
- private final String mPackageName;
- private final String mServiceName;
- private final String mId;
- private final IMediaSession2 mSessionBinder;
+ // From the return value of android.os.Process.getUidForName(String) when error
+ private static final int UID_UNKNOWN = -1;
/**
- * Constructor for the token.
+ * Constructor for the token. You can only create token for session service or library service
+ * to use by {@link MediaController2} or {@link MediaBrowser2}.
*
- * @hide
- * @param type type
+ * @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 id id
* @param serviceName name of service. Can be {@code null} if it's not an service.
- * @param sessionBinder binder for this session. Can be {@code null} if it's service.
+ * @param uid uid of the app.
+ * @hide
+ */
+ public SessionToken2(@NonNull Context context, @NonNull String packageName,
+ @NonNull String serviceName, int uid) {
+ mProvider = ApiLoader.getProvider().createSessionToken2(
+ context, this, packageName, serviceName, uid);
+ }
+
+ /**
+ * Constructor for the token.
* @hide
*/
- // TODO(jaewan): UID is also needed.
- // TODO(jaewan): Unhide
- public SessionToken2(@TokenType int type, @NonNull String packageName, @NonNull String id,
- @Nullable String serviceName, @Nullable IMediaSession2 sessionBinder) {
- // TODO(jaewan): Add sanity check.
- mType = type;
- mPackageName = packageName;
- mId = id;
- mServiceName = serviceName;
- mSessionBinder = sessionBinder;
+ public SessionToken2(@NonNull SessionToken2Provider provider) {
+ mProvider = provider;
}
+ @Override
public int hashCode() {
- final int prime = 31;
- return mType
- + prime * (mPackageName.hashCode()
- + prime * (mId.hashCode()
- + prime * ((mServiceName != null ? mServiceName.hashCode() : 0)
- + prime * (mSessionBinder != null ? mSessionBinder.asBinder().hashCode() : 0))));
+ return mProvider.hashCode_impl();
}
@Override
public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (getClass() != obj.getClass())
- return false;
- SessionToken2 other = (SessionToken2) obj;
- if (!mPackageName.equals(other.getPackageName())
- || !mServiceName.equals(other.getServiceName())
- || !mId.equals(other.getId())
- || mType != other.getType()) {
- return false;
- }
- if (mSessionBinder == other.getSessionBinder()) {
- return true;
- } else if (mSessionBinder == null || other.getSessionBinder() == null) {
- return false;
- }
- return mSessionBinder.asBinder().equals(other.getSessionBinder().asBinder());
+ return mProvider.equals_impl(obj);
}
@Override
public String toString() {
- return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
- + " service=" + mServiceName + " binder=" + mSessionBinder + "}";
+ return mProvider.toString_impl();
}
/**
- * @return package name
+ * @hide
*/
- public String getPackageName() {
- return mPackageName;
+ public SessionToken2Provider getProvider() {
+ return mProvider;
}
/**
- * @return id
+ * @return uid of the session
*/
- public String getId() {
- return mId;
+ public int getUid() {
+ return mProvider.getUid_impl();
}
/**
- * @return type of the token
- * @see #TYPE_SESSION
- * @see #TYPE_SESSION_SERVICE
+ * @return package name
*/
- public @TokenType int getType() {
- return mType;
+ public String getPackageName() {
+ return mProvider.getPackageName_impl();
}
/**
- * @return session binder.
- * @hide
+ * @return id
*/
- public @Nullable IMediaSession2 getSessionBinder() {
- return mSessionBinder;
+ public String getId() {
+ return mProvider.getId_imp();
}
/**
- * @return service name if it's session service.
- * @hide
+ * @return type of the token
+ * @see #TYPE_SESSION
+ * @see #TYPE_SESSION_SERVICE
*/
- public @Nullable String getServiceName() {
- return mServiceName;
+ 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 Bundle bundle) {
- if (bundle == null) {
- return null;
- }
- final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
- final String packageName = bundle.getString(KEY_PACKAGE_NAME);
- final String serviceName = bundle.getString(KEY_SERVICE_NAME);
- final String id = bundle.getString(KEY_ID);
- final IBinder sessionBinder = bundle.getBinder(KEY_SESSION_BINDER);
-
- // Sanity check.
- switch (type) {
- case TYPE_SESSION:
- if (!(sessionBinder instanceof IMediaSession2)) {
- throw new IllegalArgumentException("Session needs sessionBinder");
- }
- break;
- case TYPE_SESSION_SERVICE:
- if (TextUtils.isEmpty(serviceName)) {
- throw new IllegalArgumentException("Session service needs service name");
- }
- if (sessionBinder != null && !(sessionBinder instanceof IMediaSession2)) {
- throw new IllegalArgumentException("Invalid session binder");
- }
- break;
- default:
- throw new IllegalArgumentException("Invalid type");
- }
- if (TextUtils.isEmpty(packageName) || id == null) {
- throw new IllegalArgumentException("Package name nor ID cannot be null.");
- }
- // TODO(jaewan): Revisit here when we add connection callback to the session for individual
- // controller's permission check. With it, sessionBinder should be available
- // if and only if for session, not session service.
- return new SessionToken2(type, packageName, id, serviceName,
- sessionBinder != null ? IMediaSession2.Stub.asInterface(sessionBinder) : null);
+ return ApiLoader.getProvider().fromBundle_SessionToken2(bundle);
}
/**
* Create a {@link Bundle} from this token to share it across processes.
- *
* @return Bundle
- * @hide
*/
public Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putString(KEY_PACKAGE_NAME, mPackageName);
- bundle.putString(KEY_SERVICE_NAME, mServiceName);
- bundle.putString(KEY_ID, mId);
- bundle.putInt(KEY_TYPE, mType);
- bundle.putBinder(KEY_SESSION_BINDER,
- mSessionBinder != null ? mSessionBinder.asBinder() : null);
- return bundle;
+ return mProvider.toBundle_impl();
}
}
diff --git a/android/media/SubtitleData.java b/android/media/SubtitleData.java
index 3e6f6f9f..9797828d 100644
--- a/android/media/SubtitleData.java
+++ b/android/media/SubtitleData.java
@@ -16,26 +16,50 @@
package android.media;
+import android.annotation.NonNull;
import android.os.Parcel;
/**
- * @hide
- *
- * Class to hold the subtitle track's data, including:
- * <ul>
- * <li> Track index</li>
- * <li> Start time (in microseconds) of the data</li>
- * <li> Duration (in microseconds) of the data</li>
- * <li> A byte-array of the data</li>
- * </ul>
- *
- * <p> To receive the subtitle data, applications need to do the following:
- *
+ * Class encapsulating subtitle data, as received through the
+ * {@link MediaPlayer.OnSubtitleDataListener} interface.
+ * The subtitle data includes:
* <ul>
- * <li> Select a track of type MEDIA_TRACK_TYPE_SUBTITLE with {@link MediaPlayer.selectTrack(int)</li>
- * <li> Implement the {@link MediaPlayer.OnSubtitleDataListener} interface</li>
- * <li> Register the {@link MediaPlayer.OnSubtitleDataListener} callback on a MediaPlayer object</li>
+ * <li> the track index</li>
+ * <li> the start time (in microseconds) of the data</li>
+ * <li> the duration (in microseconds) of the data</li>
+ * <li> the actual data.</li>
* </ul>
+ * The data is stored in a byte-array, and is encoded in one of the supported in-band
+ * subtitle formats. The subtitle encoding is determined by the MIME type of the
+ * {@link MediaPlayer.TrackInfo} of the subtitle track, one of
+ * {@link MediaFormat#MIMETYPE_TEXT_CEA_608}, {@link MediaFormat#MIMETYPE_TEXT_CEA_708},
+ * {@link MediaFormat#MIMETYPE_TEXT_VTT}.
+ * <p>
+ * Here is an example of iterating over the tracks of a {@link MediaPlayer}, and checking which
+ * encoding is used for the subtitle tracks:
+ * <p>
+ * <pre class="prettyprint">
+ * MediaPlayer mp = new MediaPlayer();
+ * mp.setDataSource(myContentLocation);
+ * mp.prepare(); // synchronous prepare, ready to use when method returns
+ * final TrackInfo[] trackInfos = mp.getTrackInfo();
+ * for (TrackInfo info : trackInfo) {
+ * if (info.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ * final String mime = info.getFormat().getString(MediaFormat.KEY_MIME);
+ * if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mime) {
+ * // subtitle encoding is CEA 608
+ * } else if (MediaFormat.MIMETYPE_TEXT_CEA_708.equals(mime) {
+ * // subtitle encoding is CEA 708
+ * } else if (MediaFormat.MIMETYPE_TEXT_VTT.equals(mime) {
+ * // subtitle encoding is WebVTT
+ * }
+ * }
+ * }
+ * </pre>
+ * <p>
+ * See
+ * {@link MediaPlayer#setOnSubtitleDataListener(android.media.MediaPlayer.OnSubtitleDataListener, android.os.Handler)}
+ * to receive subtitle data from a MediaPlayer object.
*
* @see android.media.MediaPlayer
*/
@@ -48,25 +72,47 @@ public final class SubtitleData
private long mDurationUs;
private byte[] mData;
+ /** @hide */
public SubtitleData(Parcel parcel) {
if (!parseParcel(parcel)) {
throw new IllegalArgumentException("parseParcel() fails");
}
}
+ /**
+ * Returns the index of the MediaPlayer track which contains this subtitle data.
+ * @return an index in the array returned by {@link MediaPlayer#getTrackInfo()}.
+ */
public int getTrackIndex() {
return mTrackIndex;
}
+ /**
+ * Returns the media time at which the subtitle should be displayed, expressed in microseconds.
+ * @return the display start time for the subtitle
+ */
public long getStartTimeUs() {
return mStartTimeUs;
}
+ /**
+ * Returns the duration in microsecond during which the subtitle should be displayed.
+ * @return the display duration for the subtitle
+ */
public long getDurationUs() {
return mDurationUs;
}
- public byte[] getData() {
+ /**
+ * Returns the encoded data for the subtitle content.
+ * Encoding format depends on the subtitle type, refer to
+ * <a href="https://en.wikipedia.org/wiki/CEA-708">CEA 708</a>,
+ * <a href="https://en.wikipedia.org/wiki/EIA-608">CEA/EIA 608</a> and
+ * <a href="https://www.w3.org/TR/webvtt1/">WebVTT</a>, defined by the MIME type
+ * of the subtitle track.
+ * @return the encoded subtitle data
+ */
+ public @NonNull byte[] getData() {
return mData;
}
diff --git a/android/media/TestServiceRegistry.java b/android/media/TestServiceRegistry.java
deleted file mode 100644
index 6f5512ef..00000000
--- a/android/media/TestServiceRegistry.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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 static org.junit.Assert.fail;
-
-import android.media.MediaSession2.ControllerInfo;
-import android.media.TestUtils.SyncHandler;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.GuardedBy;
-
-/**
- * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
- * a way to control them in one place.
- * <p>
- * It only support only one service at a time.
- */
-public class TestServiceRegistry {
- public interface ServiceInstanceChangedCallback {
- void OnServiceInstanceChanged(MediaSessionService2 service);
- }
-
- @GuardedBy("TestServiceRegistry.class")
- private static TestServiceRegistry sInstance;
- @GuardedBy("TestServiceRegistry.class")
- private MediaSessionService2 mService;
- @GuardedBy("TestServiceRegistry.class")
- private SyncHandler mHandler;
- @GuardedBy("TestServiceRegistry.class")
- private ControllerInfo mOnConnectControllerInfo;
- @GuardedBy("TestServiceRegistry.class")
- private ServiceInstanceChangedCallback mCallback;
-
- public static TestServiceRegistry getInstance() {
- synchronized (TestServiceRegistry.class) {
- if (sInstance == null) {
- sInstance = new TestServiceRegistry();
- }
- return sInstance;
- }
- }
-
- public void setHandler(Handler handler) {
- synchronized (TestServiceRegistry.class) {
- mHandler = new SyncHandler(handler.getLooper());
- }
- }
-
- public void setServiceInstanceChangedCallback(ServiceInstanceChangedCallback callback) {
- synchronized (TestServiceRegistry.class) {
- mCallback = callback;
- }
- }
-
- public Handler getHandler() {
- synchronized (TestServiceRegistry.class) {
- return mHandler;
- }
- }
-
- public void setServiceInstance(MediaSessionService2 service, ControllerInfo controller) {
- synchronized (TestServiceRegistry.class) {
- if (mService != null) {
- fail("Previous service instance is still running. Clean up manually to ensure"
- + " previoulsy running service doesn't break current test");
- }
- mService = service;
- mOnConnectControllerInfo = controller;
- if (mCallback != null) {
- mCallback.OnServiceInstanceChanged(service);
- }
- }
- }
-
- public MediaSessionService2 getServiceInstance() {
- synchronized (TestServiceRegistry.class) {
- return mService;
- }
- }
-
- public ControllerInfo getOnConnectControllerInfo() {
- synchronized (TestServiceRegistry.class) {
- return mOnConnectControllerInfo;
- }
- }
-
-
- public void cleanUp() {
- synchronized (TestServiceRegistry.class) {
- final ServiceInstanceChangedCallback callback = mCallback;
- if (mService != null) {
- try {
- if (mHandler.getLooper() == Looper.myLooper()) {
- mService.getSession().close();
- } else {
- mHandler.postAndSync(() -> {
- mService.getSession().close();
- });
- }
- } catch (InterruptedException e) {
- // No-op. Service containing session will die, but shouldn't be a huge issue.
- }
- // stopSelf() would not kill service while the binder connection established by
- // bindService() exists, and close() above will do the job instead.
- // So stopSelf() isn't really needed, but just for sure.
- mService.stopSelf();
- mService = null;
- }
- if (mHandler != null) {
- mHandler.removeCallbacksAndMessages(null);
- }
- mCallback = null;
- mOnConnectControllerInfo = null;
-
- if (callback != null) {
- callback.OnServiceInstanceChanged(null);
- }
- }
- }
-}
diff --git a/android/media/TestUtils.java b/android/media/TestUtils.java
deleted file mode 100644
index 9a1fa100..00000000
--- a/android/media/TestUtils.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * 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.content.Context;
-import android.media.session.MediaSessionManager;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-
-import android.os.Looper;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-/**
- * Utilities for tests.
- */
-public final class TestUtils {
- private static final int WAIT_TIME_MS = 1000;
- private static final int WAIT_SERVICE_TIME_MS = 5000;
-
- /**
- * Creates a {@link android.media.session.PlaybackState} with the given state.
- *
- * @param state one of the PlaybackState.STATE_xxx.
- * @return a PlaybackState
- */
- public static PlaybackState2 createPlaybackState(int state) {
- return new PlaybackState2(state, 0, 0, 1.0f,
- 0, 0, null);
- }
-
- /**
- * Finds the session with id in this test package.
- *
- * @param context
- * @param id
- * @return
- */
- // TODO(jaewan): Currently not working.
- public static SessionToken2 getServiceToken(Context context, String id) {
- MediaSessionManager manager =
- (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
- List<SessionToken2> tokens = manager.getSessionServiceTokens();
- for (int i = 0; i < tokens.size(); i++) {
- SessionToken2 token = tokens.get(i);
- if (context.getPackageName().equals(token.getPackageName())
- && id.equals(token.getId())) {
- return token;
- }
- }
- fail("Failed to find service");
- return null;
- }
-
- /**
- * Compares contents of two bundles.
- *
- * @param a a bundle
- * @param b another bundle
- * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
- * incorrect if any bundle contains a bundle.
- */
- public static boolean equals(Bundle a, Bundle b) {
- if (a == b) {
- return true;
- }
- if (a == null || b == null) {
- return false;
- }
- if (!a.keySet().containsAll(b.keySet())
- || !b.keySet().containsAll(a.keySet())) {
- return false;
- }
- for (String key : a.keySet()) {
- if (!Objects.equals(a.get(key), b.get(key))) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Handler that always waits until the Runnable finishes.
- */
- public static class SyncHandler extends Handler {
- public SyncHandler(Looper looper) {
- super(looper);
- }
-
- public void postAndSync(Runnable runnable) throws InterruptedException {
- final CountDownLatch latch = new CountDownLatch(1);
- if (getLooper() == Looper.myLooper()) {
- runnable.run();
- } else {
- post(()->{
- runnable.run();
- latch.countDown();
- });
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- }
- }
- }
-}
diff --git a/android/media/VolumePolicy.java b/android/media/VolumePolicy.java
index bbcce82f..bd6667fa 100644
--- a/android/media/VolumePolicy.java
+++ b/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/android/media/VolumeProvider2.java b/android/media/VolumeProvider2.java
new file mode 100644
index 00000000..1a4608f7
--- /dev/null
+++ b/android/media/VolumeProvider2.java
@@ -0,0 +1,147 @@
+/*
+ * 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.media.update.ApiLoader;
+import android.media.update.VolumeProvider2Provider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @hide
+ * 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#updatePlayer}.
+ */
+// New version of VolumeProvider with following changes
+// - Don't implement Parcelable for updatable support.
+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(@ControlType int controlType, int maxVolume, int currentVolume) {
+ mProvider = ApiLoader.getProvider().createVolumeProvider2(
+ this, controlType, maxVolume, currentVolume);
+ }
+
+ /**
+ * @hide
+ */
+ 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/android/media/audiofx/AudioEffect.java b/android/media/audiofx/AudioEffect.java
index 7dbca3b9..21d68737 100644
--- a/android/media/audiofx/AudioEffect.java
+++ b/android/media/audiofx/AudioEffect.java
@@ -39,6 +39,7 @@ import java.util.UUID;
* <li> {@link android.media.audiofx.BassBoost}</li>
* <li> {@link android.media.audiofx.PresetReverb}</li>
* <li> {@link android.media.audiofx.EnvironmentalReverb}</li>
+ * <li> {@link android.media.audiofx.DynamicsProcessing}</li>
* </ul>
* <p>To apply the audio effect to a specific AudioTrack or MediaPlayer instance,
* the application must specify the audio session ID of that instance when creating the AudioEffect.
@@ -126,6 +127,12 @@ public class AudioEffect {
.fromString("fe3199be-aed0-413f-87bb-11260eb63cf1");
/**
+ * UUID for Dynamics Processing
+ */
+ public static final UUID EFFECT_TYPE_DYNAMICS_PROCESSING = UUID
+ .fromString("7261676f-6d75-7369-6364-28e2fd3ac39e");
+
+ /**
* Null effect UUID. Used when the UUID for effect type of
* @hide
*/
@@ -203,7 +210,8 @@ public class AudioEffect {
* {@link AudioEffect#EFFECT_TYPE_AEC}, {@link AudioEffect#EFFECT_TYPE_AGC},
* {@link AudioEffect#EFFECT_TYPE_BASS_BOOST}, {@link AudioEffect#EFFECT_TYPE_ENV_REVERB},
* {@link AudioEffect#EFFECT_TYPE_EQUALIZER}, {@link AudioEffect#EFFECT_TYPE_NS},
- * {@link AudioEffect#EFFECT_TYPE_PRESET_REVERB}, {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER}.
+ * {@link AudioEffect#EFFECT_TYPE_PRESET_REVERB}, {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER},
+ * {@link AudioEffect#EFFECT_TYPE_DYNAMICS_PROCESSING}.
* </li>
* <li>uuid: UUID for this particular implementation</li>
* <li>connectMode: {@link #EFFECT_INSERT} or {@link #EFFECT_AUXILIARY}</li>
@@ -224,7 +232,8 @@ public class AudioEffect {
* {@link AudioEffect#EFFECT_TYPE_BASS_BOOST}, {@link AudioEffect#EFFECT_TYPE_ENV_REVERB},
* {@link AudioEffect#EFFECT_TYPE_EQUALIZER}, {@link AudioEffect#EFFECT_TYPE_NS},
* {@link AudioEffect#EFFECT_TYPE_PRESET_REVERB},
- * {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER}.
+ * {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER},
+ * {@link AudioEffect#EFFECT_TYPE_DYNAMICS_PROCESSING}.
* @param uuid UUID for this particular implementation
* @param connectMode {@link #EFFECT_INSERT} or {@link #EFFECT_AUXILIARY}
* @param name human readable effect name
@@ -246,7 +255,8 @@ public class AudioEffect {
* {@link AudioEffect#EFFECT_TYPE_AGC}, {@link AudioEffect#EFFECT_TYPE_BASS_BOOST},
* {@link AudioEffect#EFFECT_TYPE_ENV_REVERB}, {@link AudioEffect#EFFECT_TYPE_EQUALIZER},
* {@link AudioEffect#EFFECT_TYPE_NS}, {@link AudioEffect#EFFECT_TYPE_PRESET_REVERB}
- * or {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER}.<br>
+ * {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER}
+ * or {@link AudioEffect#EFFECT_TYPE_DYNAMICS_PROCESSING}.<br>
* For reverberation, bass boost, EQ and virtualizer, the UUID
* corresponds to the OpenSL ES Interface ID.
*/
@@ -1344,6 +1354,34 @@ public class AudioEffect {
/**
* @hide
*/
+ public static float byteArrayToFloat(byte[] valueBuf) {
+ return byteArrayToFloat(valueBuf, 0);
+
+ }
+
+ /**
+ * @hide
+ */
+ public static float byteArrayToFloat(byte[] valueBuf, int offset) {
+ ByteBuffer converter = ByteBuffer.wrap(valueBuf);
+ converter.order(ByteOrder.nativeOrder());
+ return converter.getFloat(offset);
+
+ }
+
+ /**
+ * @hide
+ */
+ public static byte[] floatToByteArray(float value) {
+ ByteBuffer converter = ByteBuffer.allocate(4);
+ converter.order(ByteOrder.nativeOrder());
+ converter.putFloat(value);
+ return converter.array();
+ }
+
+ /**
+ * @hide
+ */
public static byte[] concatArrays(byte[]... arrays) {
int len = 0;
for (byte[] a : arrays) {
diff --git a/android/media/audiofx/DynamicsProcessing.java b/android/media/audiofx/DynamicsProcessing.java
new file mode 100644
index 00000000..4c17ae1d
--- /dev/null
+++ b/android/media/audiofx/DynamicsProcessing.java
@@ -0,0 +1,2402 @@
+/*
+ * 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.audiofx;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.AudioTrack;
+import android.media.MediaPlayer;
+import android.media.audiofx.AudioEffect;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.StringTokenizer;
+
+/**
+ * DynamicsProcessing is an audio effect for equalizing and changing dynamic range properties of the
+ * sound. It is composed of multiple stages including equalization, multi-band compression and
+ * limiter.
+ * <p>The number of bands and active stages is configurable, and most parameters can be controlled
+ * in realtime, such as gains, attack/release times, thresholds, etc.
+ * <p>The effect is instantiated and controlled by channels. Each channel has the same basic
+ * architecture, but all of their parameters are independent from other channels.
+ * <p>The basic channel configuration is:
+ * <pre>
+ *
+ * Channel 0 Channel 1 .... Channel N-1
+ * Input Input Input
+ * | | |
+ * +----v----+ +----v----+ +----v----+
+ * |inputGain| |inputGain| |inputGain|
+ * +---------+ +---------+ +---------+
+ * | | |
+ * +-----v-----+ +-----v-----+ +-----v-----+
+ * | PreEQ | | PreEQ | | PreEQ |
+ * +-----------+ +-----------+ +-----------+
+ * | | |
+ * +-----v-----+ +-----v-----+ +-----v-----+
+ * | MBC | | MBC | | MBC |
+ * +-----------+ +-----------+ +-----------+
+ * | | |
+ * +-----v-----+ +-----v-----+ +-----v-----+
+ * | PostEQ | | PostEQ | | PostEQ |
+ * +-----------+ +-----------+ +-----------+
+ * | | |
+ * +-----v-----+ +-----v-----+ +-----v-----+
+ * | Limiter | | Limiter | | Limiter |
+ * +-----------+ +-----------+ +-----------+
+ * | | |
+ * Output Output Output
+ * </pre>
+ *
+ * <p>Where the stages are:
+ * inputGain: input gain factor in decibels (dB). 0 dB means no change in level.
+ * PreEQ: Multi-band Equalizer.
+ * MBC: Multi-band Compressor
+ * PostEQ: Multi-band Equalizer
+ * Limiter: Single band compressor/limiter.
+ *
+ * <p>An application creates a DynamicsProcessing object to instantiate and control this audio
+ * effect in the audio framework. A DynamicsProcessor.Config and DynamicsProcessor.Config.Builder
+ * are available to help configure the multiple stages and each band parameters if desired.
+ * <p>See each stage documentation for further details.
+ * <p>If no Config is specified during creation, a default configuration is chosen.
+ * <p>To attach the DynamicsProcessing to a particular AudioTrack or MediaPlayer,
+ * specify the audio session ID of this AudioTrack or MediaPlayer when constructing the effect
+ * (see {@link AudioTrack#getAudioSessionId()} and {@link MediaPlayer#getAudioSessionId()}).
+ *
+ * <p>To attach the DynamicsProcessing to a particular AudioTrack or MediaPlayer, specify the audio
+ * session ID of this AudioTrack or MediaPlayer when constructing the DynamicsProcessing.
+ * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions.
+ * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling audio
+ * effects.
+ */
+
+public final class DynamicsProcessing extends AudioEffect {
+
+ private final static String TAG = "DynamicsProcessing";
+
+ // These parameter constants must be synchronized with those in
+ // /system/media/audio_effects/include/audio_effects/effect_dynamicsprocessing.h
+ private static final int PARAM_GET_CHANNEL_COUNT = 0x10;
+ private static final int PARAM_INPUT_GAIN = 0x20;
+ private static final int PARAM_ENGINE_ARCHITECTURE = 0x30;
+ private static final int PARAM_PRE_EQ = 0x40;
+ private static final int PARAM_PRE_EQ_BAND = 0x45;
+ private static final int PARAM_MBC = 0x50;
+ private static final int PARAM_MBC_BAND = 0x55;
+ private static final int PARAM_POST_EQ = 0x60;
+ private static final int PARAM_POST_EQ_BAND = 0x65;
+ private static final int PARAM_LIMITER = 0x70;
+
+ /**
+ * Index of variant that favors frequency resolution. Frequency domain based implementation.
+ */
+ public static final int VARIANT_FAVOR_FREQUENCY_RESOLUTION = 0;
+
+ /**
+ * Index of variant that favors time resolution resolution. Time domain based implementation.
+ */
+ public static final int VARIANT_FAVOR_TIME_RESOLUTION = 1;
+
+ /**
+ * Maximum expected channels to be reported by effect
+ */
+ private static final int CHANNEL_COUNT_MAX = 32;
+
+ /**
+ * Number of channels in effect architecture
+ */
+ private int mChannelCount = 0;
+
+ /**
+ * Registered listener for parameter changes.
+ */
+ private OnParameterChangeListener mParamListener = null;
+
+ /**
+ * Listener used internally to to receive raw parameter change events
+ * from AudioEffect super class
+ */
+ private BaseParameterListener mBaseParamListener = null;
+
+ /**
+ * Lock for access to mParamListener
+ */
+ private final Object mParamListenerLock = new Object();
+
+ /**
+ * Class constructor.
+ * @param audioSession system-wide unique audio session identifier. The DynamicsProcessing
+ * will be attached to the MediaPlayer or AudioTrack in the same audio session.
+ */
+ public DynamicsProcessing(int audioSession) {
+ this(0 /*priority*/, audioSession);
+ }
+
+ /**
+ * @hide
+ * Class constructor for the DynamicsProcessing audio effect.
+ * @param priority the priority level requested by the application for controlling the
+ * DynamicsProcessing engine. As the same engine can be shared by several applications,
+ * this parameter indicates how much the requesting application needs control of effect
+ * parameters. The normal priority is 0, above normal is a positive number, below normal a
+ * negative number.
+ * @param audioSession system-wide unique audio session identifier. The DynamicsProcessing
+ * will be attached to the MediaPlayer or AudioTrack in the same audio session.
+ */
+ public DynamicsProcessing(int priority, int audioSession) {
+ this(priority, audioSession, null);
+ }
+
+ /**
+ * Class constructor for the DynamicsProcessing audio effect
+ * @param priority the priority level requested by the application for controlling the
+ * DynamicsProcessing engine. As the same engine can be shared by several applications,
+ * this parameter indicates how much the requesting application needs control of effect
+ * parameters. The normal priority is 0, above normal is a positive number, below normal a
+ * negative number.
+ * @param audioSession system-wide unique audio session identifier. The DynamicsProcessing
+ * will be attached to the MediaPlayer or AudioTrack in the same audio session.
+ * @param cfg Config object used to setup the audio effect, including bands per stage, and
+ * specific parameters for each stage/band. Use
+ * {@link android.media.audiofx.DynamicsProcessing.Config.Builder} to create a
+ * Config object that suits your needs. A null cfg parameter will create and use a default
+ * configuration for the effect
+ */
+ public DynamicsProcessing(int priority, int audioSession, @Nullable Config cfg) {
+ super(EFFECT_TYPE_DYNAMICS_PROCESSING, EFFECT_TYPE_NULL, priority, audioSession);
+ if (audioSession == 0) {
+ Log.w(TAG, "WARNING: attaching a DynamicsProcessing to global output mix is"
+ + "deprecated!");
+ }
+ final Config config;
+ mChannelCount = getChannelCount();
+ if (cfg == null) {
+ //create a default configuration and effect, with the number of channels this effect has
+ DynamicsProcessing.Config.Builder builder =
+ new DynamicsProcessing.Config.Builder(
+ CONFIG_DEFAULT_VARIANT,
+ mChannelCount,
+ CONFIG_DEFAULT_USE_PREEQ,
+ CONFIG_DEFAULT_PREEQ_BANDS,
+ CONFIG_DEFAULT_USE_MBC,
+ CONFIG_DEFAULT_MBC_BANDS,
+ CONFIG_DEFAULT_USE_POSTEQ,
+ CONFIG_DEFAULT_POSTEQ_BANDS,
+ CONFIG_DEFAULT_USE_LIMITER);
+ config = builder.build();
+ } else {
+ //validate channels are ok. decide what to do: replicate channels if more
+ config = new DynamicsProcessing.Config(mChannelCount, cfg);
+ }
+
+ //configure engine
+ setEngineArchitecture(config.getVariant(),
+ config.getPreferredFrameDuration(),
+ config.isPreEqInUse(),
+ config.getPreEqBandCount(),
+ config.isMbcInUse(),
+ config.getMbcBandCount(),
+ config.isPostEqInUse(),
+ config.getPostEqBandCount(),
+ config.isLimiterInUse());
+ //update all the parameters
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ updateEngineChannelByChannelIndex(ch, config.getChannelByChannelIndex(ch));
+ }
+ }
+
+ /**
+ * Returns the Config object used to setup this effect.
+ * @return Config Current Config object used to setup this DynamicsProcessing effect.
+ */
+ public Config getConfig() {
+ //Query engine architecture to create config object
+ Number[] params = { PARAM_ENGINE_ARCHITECTURE };
+ Number[] values = { 0 /*0 variant */,
+ 0.0f /* 1 preferredFrameDuration */,
+ 0 /*2 preEqInUse */,
+ 0 /*3 preEqBandCount */,
+ 0 /*4 mbcInUse */,
+ 0 /*5 mbcBandCount*/,
+ 0 /*6 postEqInUse */,
+ 0 /*7 postEqBandCount */,
+ 0 /*8 limiterInUse */};
+ byte[] paramBytes = numberArrayToByteArray(params);
+ byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+ getParameter(paramBytes, valueBytes);
+ byteArrayToNumberArray(valueBytes, values);
+ DynamicsProcessing.Config.Builder builder =
+ new DynamicsProcessing.Config.Builder(
+ values[0].intValue(),
+ mChannelCount,
+ values[2].intValue() > 0 /*use preEQ*/,
+ values[3].intValue() /*pre eq bands*/,
+ values[4].intValue() > 0 /*use mbc*/,
+ values[5].intValue() /*mbc bands*/,
+ values[6].intValue() > 0 /*use postEQ*/,
+ values[7].intValue()/*postEq bands*/,
+ values[8].intValue() > 0 /*use Limiter*/).
+ setPreferredFrameDuration(values[1].floatValue());
+ Config config = builder.build();
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ Channel channel = queryEngineByChannelIndex(ch);
+ config.setChannelTo(ch, channel);
+ }
+ return config;
+ }
+
+
+ private static final int CONFIG_DEFAULT_VARIANT = VARIANT_FAVOR_FREQUENCY_RESOLUTION;
+ private static final boolean CONFIG_DEFAULT_USE_PREEQ = true;
+ private static final int CONFIG_DEFAULT_PREEQ_BANDS = 6;
+ private static final boolean CONFIG_DEFAULT_USE_MBC = true;
+ private static final int CONFIG_DEFAULT_MBC_BANDS = 6;
+ private static final boolean CONFIG_DEFAULT_USE_POSTEQ = true;
+ private static final int CONFIG_DEFAULT_POSTEQ_BANDS = 6;
+ private static final boolean CONFIG_DEFAULT_USE_LIMITER = true;
+
+ private static final float CHANNEL_DEFAULT_INPUT_GAIN = 0; // dB
+ private static final float CONFIG_PREFERRED_FRAME_DURATION_MS = 10.0f; //milliseconds
+
+ private static final float EQ_DEFAULT_GAIN = 0; // dB
+ private static final boolean PREEQ_DEFAULT_ENABLED = true;
+ private static final boolean POSTEQ_DEFAULT_ENABLED = true;
+
+
+ private static final boolean MBC_DEFAULT_ENABLED = true;
+ private static final float MBC_DEFAULT_ATTACK_TIME = 50; // ms
+ private static final float MBC_DEFAULT_RELEASE_TIME = 120; // ms
+ private static final float MBC_DEFAULT_RATIO = 2; // 1:N
+ private static final float MBC_DEFAULT_THRESHOLD = -30; // dB
+ private static final float MBC_DEFAULT_KNEE_WIDTH = 0; // dB
+ private static final float MBC_DEFAULT_NOISE_GATE_THRESHOLD = -80; // dB
+ private static final float MBC_DEFAULT_EXPANDER_RATIO = 1.5f; // N:1
+ private static final float MBC_DEFAULT_PRE_GAIN = 0; // dB
+ private static final float MBC_DEFAULT_POST_GAIN = 10; // dB
+
+ private static final boolean LIMITER_DEFAULT_ENABLED = true;
+ private static final int LIMITER_DEFAULT_LINK_GROUP = 0;//;
+ private static final float LIMITER_DEFAULT_ATTACK_TIME = 50; // ms
+ private static final float LIMITER_DEFAULT_RELEASE_TIME = 120; // ms
+ private static final float LIMITER_DEFAULT_RATIO = 2; // 1:N
+ private static final float LIMITER_DEFAULT_THRESHOLD = -30; // dB
+ private static final float LIMITER_DEFAULT_POST_GAIN = 10; // dB
+
+ private static final float DEFAULT_MIN_FREQUENCY = 220; // Hz
+ private static final float DEFAULT_MAX_FREQUENCY = 20000; // Hz
+ private static final float mMinFreqLog = (float)Math.log10(DEFAULT_MIN_FREQUENCY);
+ private static final float mMaxFreqLog = (float)Math.log10(DEFAULT_MAX_FREQUENCY);
+
+ /**
+ * base class for the different stages.
+ */
+ public static class Stage {
+ private boolean mInUse;
+ private boolean mEnabled;
+ /**
+ * Class constructor for stage
+ * @param inUse true if this stage is set to be used. False otherwise. Stages that are not
+ * set "inUse" at initialization time are not available to be used at any time.
+ * @param enabled true if this stage is currently used to process sound. When disabled,
+ * the stage is bypassed and the sound is copied unaltered from input to output.
+ */
+ public Stage(boolean inUse, boolean enabled) {
+ mInUse = inUse;
+ mEnabled = enabled;
+ }
+
+ /**
+ * returns enabled state of the stage
+ * @return true if stage is enabled for processing, false otherwise
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+ /**
+ * sets enabled state of the stage
+ * @param enabled true for enabled, false otherwise
+ */
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ /**
+ * returns inUse state of the stage.
+ * @return inUse state of the stage. True if this stage is currently used to process sound.
+ * When false, the stage is bypassed and the sound is copied unaltered from input to output.
+ */
+ public boolean isInUse() {
+ return mInUse;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format(" Stage InUse: %b\n", isInUse()));
+ if (isInUse()) {
+ sb.append(String.format(" Stage Enabled: %b\n", mEnabled));
+ }
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Base class for stages that hold bands
+ */
+ public static class BandStage extends Stage{
+ private int mBandCount;
+ /**
+ * Class constructor for BandStage
+ * @param inUse true if this stage is set to be used. False otherwise. Stages that are not
+ * set "inUse" at initialization time are not available to be used at any time.
+ * @param enabled true if this stage is currently used to process sound. When disabled,
+ * the stage is bypassed and the sound is copied unaltered from input to output.
+ * @param bandCount number of bands this stage will handle. If stage is not inUse, bandcount
+ * is set to 0
+ */
+ public BandStage(boolean inUse, boolean enabled, int bandCount) {
+ super(inUse, enabled);
+ mBandCount = isInUse() ? bandCount : 0;
+ }
+
+ /**
+ * gets number of bands held in this stage
+ * @return number of bands held in this stage
+ */
+ public int getBandCount() {
+ return mBandCount;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(super.toString());
+ if (isInUse()) {
+ sb.append(String.format(" Band Count: %d\n", mBandCount));
+ }
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Base class for bands
+ */
+ public static class BandBase {
+ private boolean mEnabled;
+ private float mCutoffFrequency;
+ /**
+ * Class constructor for BandBase
+ * @param enabled true if this band is currently used to process sound. When false,
+ * the band is effectively muted and sound set to zero.
+ * @param cutoffFrequency topmost frequency number (in Hz) this band will process. The
+ * effective bandwidth for the band is then computed using this and the previous band
+ * topmost frequency (or 0 Hz for band number 0). Frequencies are expected to increase with
+ * band number, thus band 0 cutoffFrequency <= band 1 cutoffFrequency, and so on.
+ */
+ public BandBase(boolean enabled, float cutoffFrequency) {
+ mEnabled = enabled;
+ mCutoffFrequency = cutoffFrequency;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format(" Enabled: %b\n", mEnabled));
+ sb.append(String.format(" CutoffFrequency: %f\n", mCutoffFrequency));
+ return sb.toString();
+ }
+
+ /**
+ * returns enabled state of the band
+ * @return true if bands is enabled for processing, false otherwise
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+ /**
+ * sets enabled state of the band
+ * @param enabled true for enabled, false otherwise
+ */
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ /**
+ * gets cutoffFrequency for this band in Hertz (Hz)
+ * @return cutoffFrequency for this band in Hertz (Hz)
+ */
+ public float getCutoffFrequency() {
+ return mCutoffFrequency;
+ }
+
+ /**
+ * sets topmost frequency number (in Hz) this band will process. The
+ * effective bandwidth for the band is then computed using this and the previous band
+ * topmost frequency (or 0 Hz for band number 0). Frequencies are expected to increase with
+ * band number, thus band 0 cutoffFrequency <= band 1 cutoffFrequency, and so on.
+ * @param frequency
+ */
+ public void setCutoffFrequency(float frequency) {
+ mCutoffFrequency = frequency;
+ }
+ }
+
+ /**
+ * Class for Equalizer Bands
+ * Equalizer bands have three controllable parameters: enabled/disabled, cutoffFrequency and
+ * gain
+ */
+ public final static class EqBand extends BandBase {
+ private float mGain;
+ /**
+ * Class constructor for EqBand
+ * @param enabled true if this band is currently used to process sound. When false,
+ * the band is effectively muted and sound set to zero.
+ * @param cutoffFrequency topmost frequency number (in Hz) this band will process. The
+ * effective bandwidth for the band is then computed using this and the previous band
+ * topmost frequency (or 0 Hz for band number 0). Frequencies are expected to increase with
+ * band number, thus band 0 cutoffFrequency <= band 1 cutoffFrequency, and so on.
+ * @param gain of equalizer band in decibels (dB). A gain of 0 dB means no change in level.
+ */
+ public EqBand(boolean enabled, float cutoffFrequency, float gain) {
+ super(enabled, cutoffFrequency);
+ mGain = gain;
+ }
+
+ /**
+ * Class constructor for EqBand
+ * @param cfg copy constructor
+ */
+ public EqBand(EqBand cfg) {
+ super(cfg.isEnabled(), cfg.getCutoffFrequency());
+ mGain = cfg.mGain;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(super.toString());
+ sb.append(String.format(" Gain: %f\n", mGain));
+ return sb.toString();
+ }
+
+ /**
+ * gets current gain of band in decibels (dB)
+ * @return current gain of band in decibels (dB)
+ */
+ public float getGain() {
+ return mGain;
+ }
+
+ /**
+ * sets current gain of band in decibels (dB)
+ * @param gain desired in decibels (db)
+ */
+ public void setGain(float gain) {
+ mGain = gain;
+ }
+ }
+
+ /**
+ * Class for Multi-Band compressor bands
+ * MBC bands have multiple controllable parameters: enabled/disabled, cutoffFrequency,
+ * attackTime, releaseTime, ratio, threshold, kneeWidth, noiseGateThreshold, expanderRatio,
+ * preGain and postGain.
+ */
+ public final static class MbcBand extends BandBase{
+ private float mAttackTime;
+ private float mReleaseTime;
+ private float mRatio;
+ private float mThreshold;
+ private float mKneeWidth;
+ private float mNoiseGateThreshold;
+ private float mExpanderRatio;
+ private float mPreGain;
+ private float mPostGain;
+ /**
+ * Class constructor for MbcBand
+ * @param enabled true if this band is currently used to process sound. When false,
+ * the band is effectively muted and sound set to zero.
+ * @param cutoffFrequency topmost frequency number (in Hz) this band will process. The
+ * effective bandwidth for the band is then computed using this and the previous band
+ * topmost frequency (or 0 Hz for band number 0). Frequencies are expected to increase with
+ * band number, thus band 0 cutoffFrequency <= band 1 cutoffFrequency, and so on.
+ * @param attackTime Attack Time for compressor in milliseconds (ms)
+ * @param releaseTime Release Time for compressor in milliseconds (ms)
+ * @param ratio Compressor ratio (1:N)
+ * @param threshold Compressor threshold measured in decibels (dB) from 0 dB Full Scale
+ * (dBFS).
+ * @param kneeWidth Width in decibels (dB) around compressor threshold point.
+ * @param noiseGateThreshold Noise gate threshold in decibels (dB) from 0 dB Full Scale
+ * (dBFS).
+ * @param expanderRatio Expander ratio (N:1) for signals below the Noise Gate Threshold.
+ * @param preGain Gain applied to the signal BEFORE the compression.
+ * @param postGain Gain applied to the signal AFTER compression.
+ */
+ public MbcBand(boolean enabled, float cutoffFrequency, float attackTime, float releaseTime,
+ float ratio, float threshold, float kneeWidth, float noiseGateThreshold,
+ float expanderRatio, float preGain, float postGain) {
+ super(enabled, cutoffFrequency);
+ mAttackTime = attackTime;
+ mReleaseTime = releaseTime;
+ mRatio = ratio;
+ mThreshold = threshold;
+ mKneeWidth = kneeWidth;
+ mNoiseGateThreshold = noiseGateThreshold;
+ mExpanderRatio = expanderRatio;
+ mPreGain = preGain;
+ mPostGain = postGain;
+ }
+
+ /**
+ * Class constructor for MbcBand
+ * @param cfg copy constructor
+ */
+ public MbcBand(MbcBand cfg) {
+ super(cfg.isEnabled(), cfg.getCutoffFrequency());
+ mAttackTime = cfg.mAttackTime;
+ mReleaseTime = cfg.mReleaseTime;
+ mRatio = cfg.mRatio;
+ mThreshold = cfg.mThreshold;
+ mKneeWidth = cfg.mKneeWidth;
+ mNoiseGateThreshold = cfg.mNoiseGateThreshold;
+ mExpanderRatio = cfg.mExpanderRatio;
+ mPreGain = cfg.mPreGain;
+ mPostGain = cfg.mPostGain;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(super.toString());
+ sb.append(String.format(" AttackTime: %f (ms)\n", mAttackTime));
+ sb.append(String.format(" ReleaseTime: %f (ms)\n", mReleaseTime));
+ sb.append(String.format(" Ratio: 1:%f\n", mRatio));
+ sb.append(String.format(" Threshold: %f (dB)\n", mThreshold));
+ sb.append(String.format(" NoiseGateThreshold: %f(dB)\n", mNoiseGateThreshold));
+ sb.append(String.format(" ExpanderRatio: %f:1\n", mExpanderRatio));
+ sb.append(String.format(" PreGain: %f (dB)\n", mPreGain));
+ sb.append(String.format(" PostGain: %f (dB)\n", mPostGain));
+ return sb.toString();
+ }
+
+ /**
+ * gets attack time for compressor in milliseconds (ms)
+ * @return attack time for compressor in milliseconds (ms)
+ */
+ public float getAttackTime() { return mAttackTime; }
+ /**
+ * sets attack time for compressor in milliseconds (ms)
+ * @param attackTime desired for compressor in milliseconds (ms)
+ */
+ public void setAttackTime(float attackTime) { mAttackTime = attackTime; }
+ /**
+ * gets release time for compressor in milliseconds (ms)
+ * @return release time for compressor in milliseconds (ms)
+ */
+ public float getReleaseTime() { return mReleaseTime; }
+ /**
+ * sets release time for compressor in milliseconds (ms)
+ * @param releaseTime desired for compressor in milliseconds (ms)
+ */
+ public void setReleaseTime(float releaseTime) { mReleaseTime = releaseTime; }
+ /**
+ * gets the compressor ratio (1:N)
+ * @return compressor ratio (1:N)
+ */
+ public float getRatio() { return mRatio; }
+ /**
+ * sets compressor ratio (1:N)
+ * @param ratio desired for the compressor (1:N)
+ */
+ public void setRatio(float ratio) { mRatio = ratio; }
+ /**
+ * gets the compressor threshold measured in decibels (dB) from 0 dB Full Scale (dBFS).
+ * Thresholds are negative. A threshold of 0 dB means no compression will take place.
+ * @return compressor threshold in decibels (dB)
+ */
+ public float getThreshold() { return mThreshold; }
+ /**
+ * sets the compressor threshold measured in decibels (dB) from 0 dB Full Scale (dBFS).
+ * Thresholds are negative. A threshold of 0 dB means no compression will take place.
+ * @param threshold desired for compressor in decibels(dB)
+ */
+ public void setThreshold(float threshold) { mThreshold = threshold; }
+ /**
+ * get Knee Width in decibels (dB) around compressor threshold point. Widths are always
+ * positive, with higher values representing a wider area of transition from the linear zone
+ * to the compression zone. A knee of 0 dB means a more abrupt transition.
+ * @return Knee Width in decibels (dB)
+ */
+ public float getKneeWidth() { return mKneeWidth; }
+ /**
+ * sets knee width in decibels (dB). See
+ * {@link android.media.audiofx.DynamicsProcessing.MbcBand#getKneeWidth} for more
+ * information.
+ * @param kneeWidth desired in decibels (dB)
+ */
+ public void setKneeWidth(float kneeWidth) { mKneeWidth = kneeWidth; }
+ /**
+ * gets the noise gate threshold in decibels (dB) from 0 dB Full Scale (dBFS). Noise gate
+ * thresholds are negative. Signals below this level will be expanded according the
+ * expanderRatio parameter. A Noise Gate Threshold of -75 dB means very quiet signals might
+ * be effectively removed from the signal.
+ * @return Noise Gate Threshold in decibels (dB)
+ */
+ public float getNoiseGateThreshold() { return mNoiseGateThreshold; }
+ /**
+ * sets noise gate threshod in decibels (dB). See
+ * {@link android.media.audiofx.DynamicsProcessing.MbcBand#getNoiseGateThreshold} for more
+ * information.
+ * @param noiseGateThreshold desired in decibels (dB)
+ */
+ public void setNoiseGateThreshold(float noiseGateThreshold) {
+ mNoiseGateThreshold = noiseGateThreshold; }
+ /**
+ * gets Expander ratio (N:1) for signals below the Noise Gate Threshold.
+ * @return Expander ratio (N:1)
+ */
+ public float getExpanderRatio() { return mExpanderRatio; }
+ /**
+ * sets Expander ratio (N:1) for signals below the Noise Gate Threshold.
+ * @param expanderRatio desired expander ratio (N:1)
+ */
+ public void setExpanderRatio(float expanderRatio) { mExpanderRatio = expanderRatio; }
+ /**
+ * gets the gain applied to the signal BEFORE the compression. Measured in decibels (dB)
+ * where 0 dB means no level change.
+ * @return preGain value in decibels (dB)
+ */
+ public float getPreGain() { return mPreGain; }
+ /**
+ * sets the gain to be applied to the signal BEFORE the compression, measured in decibels
+ * (dB), where 0 dB means no level change.
+ * @param preGain desired in decibels (dB)
+ */
+ public void setPreGain(float preGain) { mPreGain = preGain; }
+ /**
+ * gets the gain applied to the signal AFTER compression. Measured in decibels (dB) where 0
+ * dB means no level change
+ * @return postGain value in decibels (dB)
+ */
+ public float getPostGain() { return mPostGain; }
+ /**
+ * sets the gain to be applied to the siganl AFTER the compression. Measured in decibels
+ * (dB), where 0 dB means no level change.
+ * @param postGain desired value in decibels (dB)
+ */
+ public void setPostGain(float postGain) { mPostGain = postGain; }
+ }
+
+ /**
+ * Class for Equalizer stage
+ */
+ public final static class Eq extends BandStage {
+ private final EqBand[] mBands;
+ /**
+ * Class constructor for Equalizer (Eq) stage
+ * @param inUse true if Eq stage will be used, false otherwise.
+ * @param enabled true if Eq stage is enabled/disabled. This can be changed while effect is
+ * running
+ * @param bandCount number of bands for this Equalizer stage. Can't be changed while effect
+ * is running
+ */
+ public Eq(boolean inUse, boolean enabled, int bandCount) {
+ super(inUse, enabled, bandCount);
+ if (isInUse()) {
+ mBands = new EqBand[bandCount];
+ for (int b = 0; b < bandCount; b++) {
+ float freq = DEFAULT_MAX_FREQUENCY;
+ if (bandCount > 1) {
+ freq = (float)Math.pow(10, mMinFreqLog +
+ b * (mMaxFreqLog - mMinFreqLog)/(bandCount -1));
+ }
+ mBands[b] = new EqBand(true, freq, EQ_DEFAULT_GAIN);
+ }
+ } else {
+ mBands = null;
+ }
+ }
+ /**
+ * Class constructor for Eq stage
+ * @param cfg copy constructor
+ */
+ public Eq(Eq cfg) {
+ super(cfg.isInUse(), cfg.isEnabled(), cfg.getBandCount());
+ if (isInUse()) {
+ mBands = new EqBand[cfg.mBands.length];
+ for (int b = 0; b < mBands.length; b++) {
+ mBands[b] = new EqBand(cfg.mBands[b]);
+ }
+ } else {
+ mBands = null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(super.toString());
+ if (isInUse()) {
+ sb.append("--->EqBands: " + mBands.length + "\n");
+ for (int b = 0; b < mBands.length; b++) {
+ sb.append(String.format(" Band %d\n", b));
+ sb.append(mBands[b].toString());
+ }
+ }
+ return sb.toString();
+ }
+ /**
+ * Helper function to check if band index is within range
+ * @param band index to check
+ */
+ private void checkBand(int band) {
+ if (mBands == null || band < 0 || band >= mBands.length) {
+ throw new IllegalArgumentException("band index " + band +" out of bounds");
+ }
+ }
+ /**
+ * Sets EqBand object for given band index
+ * @param band index of band to be modified
+ * @param bandCfg EqBand object.
+ */
+ public void setBand(int band, EqBand bandCfg) {
+ checkBand(band);
+ mBands[band] = new EqBand(bandCfg);
+ }
+ /**
+ * Gets EqBand object for band of interest.
+ * @param band index of band of interest
+ * @return EqBand Object
+ */
+ public EqBand getBand(int band) {
+ checkBand(band);
+ return mBands[band];
+ }
+ }
+
+ /**
+ * Class for Multi-Band Compressor (MBC) stage
+ */
+ public final static class Mbc extends BandStage {
+ private final MbcBand[] mBands;
+ /**
+ * Constructor for Multi-Band Compressor (MBC) stage
+ * @param inUse true if MBC stage will be used, false otherwise.
+ * @param enabled true if MBC stage is enabled/disabled. This can be changed while effect
+ * is running
+ * @param bandCount number of bands for this MBC stage. Can't be changed while effect is
+ * running
+ */
+ public Mbc(boolean inUse, boolean enabled, int bandCount) {
+ super(inUse, enabled, bandCount);
+ if (isInUse()) {
+ mBands = new MbcBand[bandCount];
+ for (int b = 0; b < bandCount; b++) {
+ float freq = DEFAULT_MAX_FREQUENCY;
+ if (bandCount > 1) {
+ freq = (float)Math.pow(10, mMinFreqLog +
+ b * (mMaxFreqLog - mMinFreqLog)/(bandCount -1));
+ }
+ mBands[b] = new MbcBand(true, freq, MBC_DEFAULT_ATTACK_TIME,
+ MBC_DEFAULT_RELEASE_TIME, MBC_DEFAULT_RATIO,
+ MBC_DEFAULT_THRESHOLD, MBC_DEFAULT_KNEE_WIDTH,
+ MBC_DEFAULT_NOISE_GATE_THRESHOLD, MBC_DEFAULT_EXPANDER_RATIO,
+ MBC_DEFAULT_PRE_GAIN, MBC_DEFAULT_POST_GAIN);
+ }
+ } else {
+ mBands = null;
+ }
+ }
+ /**
+ * Class constructor for MBC stage
+ * @param cfg copy constructor
+ */
+ public Mbc(Mbc cfg) {
+ super(cfg.isInUse(), cfg.isEnabled(), cfg.getBandCount());
+ if (isInUse()) {
+ mBands = new MbcBand[cfg.mBands.length];
+ for (int b = 0; b < mBands.length; b++) {
+ mBands[b] = new MbcBand(cfg.mBands[b]);
+ }
+ } else {
+ mBands = null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(super.toString());
+ if (isInUse()) {
+ sb.append("--->MbcBands: " + mBands.length + "\n");
+ for (int b = 0; b < mBands.length; b++) {
+ sb.append(String.format(" Band %d\n", b));
+ sb.append(mBands[b].toString());
+ }
+ }
+ return sb.toString();
+ }
+ /**
+ * Helper function to check if band index is within range
+ * @param band index to check
+ */
+ private void checkBand(int band) {
+ if (mBands == null || band < 0 || band >= mBands.length) {
+ throw new IllegalArgumentException("band index " + band +" out of bounds");
+ }
+ }
+ /**
+ * Sets MbcBand object for given band index
+ * @param band index of band to be modified
+ * @param bandCfg MbcBand object.
+ */
+ public void setBand(int band, MbcBand bandCfg) {
+ checkBand(band);
+ mBands[band] = new MbcBand(bandCfg);
+ }
+ /**
+ * Gets MbcBand object for band of interest.
+ * @param band index of band of interest
+ * @return MbcBand Object
+ */
+ public MbcBand getBand(int band) {
+ checkBand(band);
+ return mBands[band];
+ }
+ }
+
+ /**
+ * Class for Limiter Stage
+ * Limiter is a single band compressor at the end of the processing chain, commonly used to
+ * protect the signal from overloading and distortion. Limiters have multiple controllable
+ * parameters: enabled/disabled, linkGroup, attackTime, releaseTime, ratio, threshold, and
+ * postGain.
+ * <p>Limiters can be linked in groups across multiple channels. Linked limiters will trigger
+ * the same limiting if any of the linked limiters starts compressing.
+ */
+ public final static class Limiter extends Stage {
+ private int mLinkGroup;
+ private float mAttackTime;
+ private float mReleaseTime;
+ private float mRatio;
+ private float mThreshold;
+ private float mPostGain;
+
+ /**
+ * Class constructor for Limiter Stage
+ * @param inUse true if MBC stage will be used, false otherwise.
+ * @param enabled true if MBC stage is enabled/disabled. This can be changed while effect
+ * is running
+ * @param linkGroup index of group assigned to this Limiter. Only limiters that share the
+ * same linkGroup index will react together.
+ * @param attackTime Attack Time for limiter compressor in milliseconds (ms)
+ * @param releaseTime Release Time for limiter compressor in milliseconds (ms)
+ * @param ratio Limiter Compressor ratio (1:N)
+ * @param threshold Limiter Compressor threshold measured in decibels (dB) from 0 dB Full
+ * Scale (dBFS).
+ * @param postGain Gain applied to the signal AFTER compression.
+ */
+ public Limiter(boolean inUse, boolean enabled, int linkGroup, float attackTime,
+ float releaseTime, float ratio, float threshold, float postGain) {
+ super(inUse, enabled);
+ mLinkGroup = linkGroup;
+ mAttackTime = attackTime;
+ mReleaseTime = releaseTime;
+ mRatio = ratio;
+ mThreshold = threshold;
+ mPostGain = postGain;
+ }
+
+ /**
+ * Class Constructor for Limiter
+ * @param cfg copy constructor
+ */
+ public Limiter(Limiter cfg) {
+ super(cfg.isInUse(), cfg.isEnabled());
+ mLinkGroup = cfg.mLinkGroup;
+ mAttackTime = cfg.mAttackTime;
+ mReleaseTime = cfg.mReleaseTime;
+ mRatio = cfg.mRatio;
+ mThreshold = cfg.mThreshold;
+ mPostGain = cfg.mPostGain;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(super.toString());
+ if (isInUse()) {
+ sb.append(String.format(" LinkGroup: %d (group)\n", mLinkGroup));
+ sb.append(String.format(" AttackTime: %f (ms)\n", mAttackTime));
+ sb.append(String.format(" ReleaseTime: %f (ms)\n", mReleaseTime));
+ sb.append(String.format(" Ratio: 1:%f\n", mRatio));
+ sb.append(String.format(" Threshold: %f (dB)\n", mThreshold));
+ sb.append(String.format(" PostGain: %f (dB)\n", mPostGain));
+ }
+ return sb.toString();
+ }
+ /**
+ * Gets the linkGroup index for this Limiter Stage. Only limiters that share the same
+ * linkGroup index will react together.
+ * @return linkGroup index.
+ */
+ public int getLinkGroup() { return mLinkGroup; }
+ /**
+ * Sets the linkGroup index for this limiter Stage.
+ * @param linkGroup desired linkGroup index
+ */
+ public void setLinkGroup(int linkGroup) { mLinkGroup = linkGroup; }
+ /**
+ * gets attack time for limiter compressor in milliseconds (ms)
+ * @return attack time for limiter compressor in milliseconds (ms)
+ */
+ public float getAttackTime() { return mAttackTime; }
+ /**
+ * sets attack time for limiter compressor in milliseconds (ms)
+ * @param attackTime desired for limiter compressor in milliseconds (ms)
+ */
+ public void setAttackTime(float attackTime) { mAttackTime = attackTime; }
+ /**
+ * gets release time for limiter compressor in milliseconds (ms)
+ * @return release time for limiter compressor in milliseconds (ms)
+ */
+ public float getReleaseTime() { return mReleaseTime; }
+ /**
+ * sets release time for limiter compressor in milliseconds (ms)
+ * @param releaseTime desired for limiter compressor in milliseconds (ms)
+ */
+ public void setReleaseTime(float releaseTime) { mReleaseTime = releaseTime; }
+ /**
+ * gets the limiter compressor ratio (1:N)
+ * @return limiter compressor ratio (1:N)
+ */
+ public float getRatio() { return mRatio; }
+ /**
+ * sets limiter compressor ratio (1:N)
+ * @param ratio desired for the limiter compressor (1:N)
+ */
+ public void setRatio(float ratio) { mRatio = ratio; }
+ /**
+ * gets the limiter compressor threshold measured in decibels (dB) from 0 dB Full Scale
+ * (dBFS). Thresholds are negative. A threshold of 0 dB means no limiting will take place.
+ * @return limiter compressor threshold in decibels (dB)
+ */
+ public float getThreshold() { return mThreshold; }
+ /**
+ * sets the limiter compressor threshold measured in decibels (dB) from 0 dB Full Scale
+ * (dBFS). Thresholds are negative. A threshold of 0 dB means no limiting will take place.
+ * @param threshold desired for limiter compressor in decibels(dB)
+ */
+ public void setThreshold(float threshold) { mThreshold = threshold; }
+ /**
+ * gets the gain applied to the signal AFTER limiting. Measured in decibels (dB) where 0
+ * dB means no level change
+ * @return postGain value in decibels (dB)
+ */
+ public float getPostGain() { return mPostGain; }
+ /**
+ * sets the gain to be applied to the siganl AFTER the limiter. Measured in decibels
+ * (dB), where 0 dB means no level change.
+ * @param postGain desired value in decibels (dB)
+ */
+ public void setPostGain(float postGain) { mPostGain = postGain; }
+ }
+
+ /**
+ * Class for Channel configuration parameters. It is composed of multiple stages, which can be
+ * used/enabled independently. Stages not used or disabled will be bypassed and the sound would
+ * be unaffected by them.
+ */
+ public final static class Channel {
+ private float mInputGain;
+ private Eq mPreEq;
+ private Mbc mMbc;
+ private Eq mPostEq;
+ private Limiter mLimiter;
+
+ /**
+ * Class constructor for Channel configuration.
+ * @param inputGain value in decibels (dB) of level change applied to the audio before
+ * processing. A value of 0 dB means no change.
+ * @param preEqInUse true if PreEq stage will be used, false otherwise. This can't be
+ * changed later.
+ * @param preEqBandCount number of bands for PreEq stage. This can't be changed later.
+ * @param mbcInUse true if Mbc stage will be used, false otherwise. This can't be changed
+ * later.
+ * @param mbcBandCount number of bands for Mbc stage. This can't be changed later.
+ * @param postEqInUse true if PostEq stage will be used, false otherwise. This can't be
+ * changed later.
+ * @param postEqBandCount number of bands for PostEq stage. This can't be changed later.
+ * @param limiterInUse true if Limiter stage will be used, false otherwise. This can't be
+ * changed later.
+ */
+ public Channel (float inputGain,
+ boolean preEqInUse, int preEqBandCount,
+ boolean mbcInUse, int mbcBandCount,
+ boolean postEqInUse, int postEqBandCount,
+ boolean limiterInUse) {
+ mInputGain = inputGain;
+ mPreEq = new Eq(preEqInUse, PREEQ_DEFAULT_ENABLED, preEqBandCount);
+ mMbc = new Mbc(mbcInUse, MBC_DEFAULT_ENABLED, mbcBandCount);
+ mPostEq = new Eq(postEqInUse, POSTEQ_DEFAULT_ENABLED,
+ postEqBandCount);
+ mLimiter = new Limiter(limiterInUse,
+ LIMITER_DEFAULT_ENABLED, LIMITER_DEFAULT_LINK_GROUP,
+ LIMITER_DEFAULT_ATTACK_TIME, LIMITER_DEFAULT_RELEASE_TIME,
+ LIMITER_DEFAULT_RATIO, LIMITER_DEFAULT_THRESHOLD, LIMITER_DEFAULT_POST_GAIN);
+ }
+
+ /**
+ * Class constructor for Channel configuration
+ * @param cfg copy constructor
+ */
+ public Channel(Channel cfg) {
+ mInputGain = cfg.mInputGain;
+ mPreEq = new Eq(cfg.mPreEq);
+ mMbc = new Mbc(cfg.mMbc);
+ mPostEq = new Eq(cfg.mPostEq);
+ mLimiter = new Limiter(cfg.mLimiter);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format(" InputGain: %f\n", mInputGain));
+ sb.append("-->PreEq\n");
+ sb.append(mPreEq.toString());
+ sb.append("-->MBC\n");
+ sb.append(mMbc.toString());
+ sb.append("-->PostEq\n");
+ sb.append(mPostEq.toString());
+ sb.append("-->Limiter\n");
+ sb.append(mLimiter.toString());
+ return sb.toString();
+ }
+ /**
+ * Gets inputGain value in decibels (dB). 0 dB means no change;
+ * @return gain value in decibels (dB)
+ */
+ public float getInputGain() {
+ return mInputGain;
+ }
+ /**
+ * Sets inputGain value in decibels (dB). 0 dB means no change;
+ * @param inputGain desired gain value in decibels (dB)
+ */
+ public void setInputGain(float inputGain) {
+ mInputGain = inputGain;
+ }
+
+ /**
+ * Gets PreEq configuration stage
+ * @return PreEq configuration stage
+ */
+ public Eq getPreEq() {
+ return mPreEq;
+ }
+ /**
+ * Sets PreEq configuration stage. New PreEq stage must have the same number of bands than
+ * original PreEq stage.
+ * @param preEq configuration
+ */
+ public void setPreEq(Eq preEq) {
+ if (preEq.getBandCount() != mPreEq.getBandCount()) {
+ throw new IllegalArgumentException("PreEqBandCount changed from " +
+ mPreEq.getBandCount() + " to " + preEq.getBandCount());
+ }
+ mPreEq = new Eq(preEq);
+ }
+ /**
+ * Gets EqBand for PreEq stage for given band index.
+ * @param band index of band of interest from PreEq stage
+ * @return EqBand configuration
+ */
+ public EqBand getPreEqBand(int band) {
+ return mPreEq.getBand(band);
+ }
+ /**
+ * Sets EqBand for PreEq stage for given band index
+ * @param band index of band of interest from PreEq stage
+ * @param preEqBand configuration to be set.
+ */
+ public void setPreEqBand(int band, EqBand preEqBand) {
+ mPreEq.setBand(band, preEqBand);
+ }
+
+ /**
+ * Gets Mbc configuration stage
+ * @return Mbc configuration stage
+ */
+ public Mbc getMbc() {
+ return mMbc;
+ }
+ /**
+ * Sets Mbc configuration stage. New Mbc stage must have the same number of bands than
+ * original Mbc stage.
+ * @param mbc
+ */
+ public void setMbc(Mbc mbc) {
+ if (mbc.getBandCount() != mMbc.getBandCount()) {
+ throw new IllegalArgumentException("MbcBandCount changed from " +
+ mMbc.getBandCount() + " to " + mbc.getBandCount());
+ }
+ mMbc = new Mbc(mbc);
+ }
+ /**
+ * Gets MbcBand configuration for Mbc stage, for given band index.
+ * @param band index of band of interest from Mbc stage
+ * @return MbcBand configuration
+ */
+ public MbcBand getMbcBand(int band) {
+ return mMbc.getBand(band);
+ }
+ /**
+ * Sets MbcBand for Mbc stage for given band index
+ * @param band index of band of interest from Mbc Stage
+ * @param mbcBand configuration to be set
+ */
+ public void setMbcBand(int band, MbcBand mbcBand) {
+ mMbc.setBand(band, mbcBand);
+ }
+
+ /**
+ * Gets PostEq configuration stage
+ * @return PostEq configuration stage
+ */
+ public Eq getPostEq() {
+ return mPostEq;
+ }
+ /**
+ * Sets PostEq configuration stage. New PostEq stage must have the same number of bands than
+ * original PostEq stage.
+ * @param postEq configuration
+ */
+ public void setPostEq(Eq postEq) {
+ if (postEq.getBandCount() != mPostEq.getBandCount()) {
+ throw new IllegalArgumentException("PostEqBandCount changed from " +
+ mPostEq.getBandCount() + " to " + postEq.getBandCount());
+ }
+ mPostEq = new Eq(postEq);
+ }
+ /**
+ * Gets EqBand for PostEq stage for given band index.
+ * @param band index of band of interest from PostEq stage
+ * @return EqBand configuration
+ */
+ public EqBand getPostEqBand(int band) {
+ return mPostEq.getBand(band);
+ }
+ /**
+ * Sets EqBand for PostEq stage for given band index
+ * @param band index of band of interest from PostEq stage
+ * @param postEqBand configuration to be set.
+ */
+ public void setPostEqBand(int band, EqBand postEqBand) {
+ mPostEq.setBand(band, postEqBand);
+ }
+
+ /**
+ * Gets Limiter configuration stage
+ * @return Limiter configuration stage
+ */
+ public Limiter getLimiter() {
+ return mLimiter;
+ }
+ /**
+ * Sets Limiter configuration stage.
+ * @param limiter configuration stage.
+ */
+ public void setLimiter(Limiter limiter) {
+ mLimiter = new Limiter(limiter);
+ }
+ }
+
+ /**
+ * Class for Config object, used by DynamicsProcessing to configure and update the audio effect.
+ * use Builder to instantiate objects of this type.
+ */
+ public final static class Config {
+ private final int mVariant;
+ private final int mChannelCount;
+ private final boolean mPreEqInUse;
+ private final int mPreEqBandCount;
+ private final boolean mMbcInUse;
+ private final int mMbcBandCount;
+ private final boolean mPostEqInUse;
+ private final int mPostEqBandCount;
+ private final boolean mLimiterInUse;
+ private final float mPreferredFrameDuration;
+ private final Channel[] mChannel;
+
+ /**
+ * @hide
+ * Class constructor for config. None of these parameters can be changed later.
+ * @param variant index of variant used for effect engine. See
+ * {@link #VARIANT_FAVOR_FREQUENCY_RESOLUTION} and {@link #VARIANT_FAVOR_TIME_RESOLUTION}.
+ * @param frameDurationMs preferred frame duration in milliseconds (ms).
+ * @param channelCount Number of channels to be configured.
+ * @param preEqInUse true if PreEq stage will be used, false otherwise.
+ * @param preEqBandCount number of bands for PreEq stage.
+ * @param mbcInUse true if Mbc stage will be used, false otherwise.
+ * @param mbcBandCount number of bands for Mbc stage.
+ * @param postEqInUse true if PostEq stage will be used, false otherwise.
+ * @param postEqBandCount number of bands for PostEq stage.
+ * @param limiterInUse true if Limiter stage will be used, false otherwise.
+ * @param channel array of Channel objects to be used for this configuration.
+ */
+ public Config(int variant, float frameDurationMs, int channelCount,
+ boolean preEqInUse, int preEqBandCount,
+ boolean mbcInUse, int mbcBandCount,
+ boolean postEqInUse, int postEqBandCount,
+ boolean limiterInUse,
+ Channel[] channel) {
+ mVariant = variant;
+ mPreferredFrameDuration = frameDurationMs;
+ mChannelCount = channelCount;
+ mPreEqInUse = preEqInUse;
+ mPreEqBandCount = preEqBandCount;
+ mMbcInUse = mbcInUse;
+ mMbcBandCount = mbcBandCount;
+ mPostEqInUse = postEqInUse;
+ mPostEqBandCount = postEqBandCount;
+ mLimiterInUse = limiterInUse;
+
+ mChannel = new Channel[mChannelCount];
+ //check if channelconfig is null or has less channels than channel count.
+ //options: fill the missing with default options.
+ // or fail?
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ if (ch < channel.length) {
+ mChannel[ch] = new Channel(channel[ch]); //copy create
+ } else {
+ //create a new one from scratch? //fail?
+ }
+ }
+ }
+ //a version that will scale to necessary number of channels
+ /**
+ * @hide
+ * Class constructor for Configuration.
+ * @param channelCount limit configuration to this number of channels. if channelCount is
+ * greater than number of channels in cfg, the constructor will duplicate the last channel
+ * found as many times as necessary to create a Config with channelCount number of channels.
+ * If channelCount is less than channels in cfg, the extra channels in cfg will be ignored.
+ * @param cfg copy constructor paremter.
+ */
+ public Config(int channelCount, Config cfg) {
+ mVariant = cfg.mVariant;
+ mPreferredFrameDuration = cfg.mPreferredFrameDuration;
+ mChannelCount = cfg.mChannelCount;
+ mPreEqInUse = cfg.mPreEqInUse;
+ mPreEqBandCount = cfg.mPreEqBandCount;
+ mMbcInUse = cfg.mMbcInUse;
+ mMbcBandCount = cfg.mMbcBandCount;
+ mPostEqInUse = cfg.mPostEqInUse;
+ mPostEqBandCount = cfg.mPostEqBandCount;
+ mLimiterInUse = cfg.mLimiterInUse;
+
+ if (mChannelCount != cfg.mChannel.length) {
+ throw new IllegalArgumentException("configuration channel counts differ " +
+ mChannelCount + " !=" + cfg.mChannel.length);
+ }
+ if (channelCount < 1) {
+ throw new IllegalArgumentException("channel resizing less than 1 not allowed");
+ }
+
+ mChannel = new Channel[channelCount];
+ for (int ch = 0; ch < channelCount; ch++) {
+ if (ch < mChannelCount) {
+ mChannel[ch] = new Channel(cfg.mChannel[ch]);
+ } else {
+ //duplicate last
+ mChannel[ch] = new Channel(cfg.mChannel[mChannelCount-1]);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * Class constructor for Config
+ * @param cfg Configuration object copy constructor
+ */
+ public Config(@NonNull Config cfg) {
+ this(cfg.mChannelCount, cfg);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("Variant: %d\n", mVariant));
+ sb.append(String.format("PreferredFrameDuration: %f\n", mPreferredFrameDuration));
+ sb.append(String.format("ChannelCount: %d\n", mChannelCount));
+ sb.append(String.format("PreEq inUse: %b, bandCount:%d\n",mPreEqInUse,
+ mPreEqBandCount));
+ sb.append(String.format("Mbc inUse: %b, bandCount: %d\n",mMbcInUse, mMbcBandCount));
+ sb.append(String.format("PostEq inUse: %b, bandCount: %d\n", mPostEqInUse,
+ mPostEqBandCount));
+ sb.append(String.format("Limiter inUse: %b\n", mLimiterInUse));
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ sb.append(String.format("==Channel %d\n", ch));
+ sb.append(mChannel[ch].toString());
+ }
+ return sb.toString();
+ }
+ private void checkChannel(int channelIndex) {
+ if (channelIndex < 0 || channelIndex >= mChannel.length) {
+ throw new IllegalArgumentException("ChannelIndex out of bounds");
+ }
+ }
+
+ //getters and setters
+ /**
+ * Gets variant for effect engine See {@link #VARIANT_FAVOR_FREQUENCY_RESOLUTION} and
+ * {@link #VARIANT_FAVOR_TIME_RESOLUTION}.
+ * @return variant of effect engine
+ */
+ public int getVariant() {
+ return mVariant;
+ }
+ /**
+ * Gets preferred frame duration in milliseconds (ms).
+ * @return preferred frame duration in milliseconds (ms)
+ */
+ public float getPreferredFrameDuration() {
+ return mPreferredFrameDuration;
+ }
+ /**
+ * Gets if preEq stage is in use
+ * @return true if preEq stage is in use;
+ */
+ public boolean isPreEqInUse() {
+ return mPreEqInUse;
+ }
+ /**
+ * Gets number of bands configured for the PreEq stage.
+ * @return number of bands configured for the PreEq stage.
+ */
+ public int getPreEqBandCount() {
+ return mPreEqBandCount;
+ }
+ /**
+ * Gets if Mbc stage is in use
+ * @return true if Mbc stage is in use;
+ */
+ public boolean isMbcInUse() {
+ return mMbcInUse;
+ }
+ /**
+ * Gets number of bands configured for the Mbc stage.
+ * @return number of bands configured for the Mbc stage.
+ */
+ public int getMbcBandCount() {
+ return mMbcBandCount;
+ }
+ /**
+ * Gets if PostEq stage is in use
+ * @return true if PostEq stage is in use;
+ */
+ public boolean isPostEqInUse() {
+ return mPostEqInUse;
+ }
+ /**
+ * Gets number of bands configured for the PostEq stage.
+ * @return number of bands configured for the PostEq stage.
+ */
+ public int getPostEqBandCount() {
+ return mPostEqBandCount;
+ }
+ /**
+ * Gets if Limiter stage is in use
+ * @return true if Limiter stage is in use;
+ */
+ public boolean isLimiterInUse() {
+ return mLimiterInUse;
+ }
+
+ //channel
+ /**
+ * Gets the Channel configuration object by using the channel index
+ * @param channelIndex of desired Channel object
+ * @return Channel configuration object
+ */
+ public Channel getChannelByChannelIndex(int channelIndex) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex];
+ }
+
+ /**
+ * Sets the chosen Channel object in the selected channelIndex
+ * Note that all the stages should have the same number of bands than the existing Channel
+ * object.
+ * @param channelIndex index of channel to be replaced
+ * @param channel Channel configuration object to be set
+ */
+ public void setChannelTo(int channelIndex, Channel channel) {
+ checkChannel(channelIndex);
+ //check all things are compatible
+ if (mMbcBandCount != channel.getMbc().getBandCount()) {
+ throw new IllegalArgumentException("MbcBandCount changed from " +
+ mMbcBandCount + " to " + channel.getPreEq().getBandCount());
+ }
+ if (mPreEqBandCount != channel.getPreEq().getBandCount()) {
+ throw new IllegalArgumentException("PreEqBandCount changed from " +
+ mPreEqBandCount + " to " + channel.getPreEq().getBandCount());
+ }
+ if (mPostEqBandCount != channel.getPostEq().getBandCount()) {
+ throw new IllegalArgumentException("PostEqBandCount changed from " +
+ mPostEqBandCount + " to " + channel.getPostEq().getBandCount());
+ }
+ mChannel[channelIndex] = new Channel(channel);
+ }
+
+ /**
+ * Sets ALL channels to the chosen Channel object. Note that all the stages should have the
+ * same number of bands than the existing ones.
+ * @param channel Channel configuration object to be set.
+ */
+ public void setAllChannelsTo(Channel channel) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ setChannelTo(ch, channel);
+ }
+ }
+
+ //===channel params
+ /**
+ * Gets inputGain value in decibels (dB) for channel indicated by channelIndex
+ * @param channelIndex index of channel of interest
+ * @return inputGain value in decibels (dB). 0 dB means no change.
+ */
+ public float getInputGainByChannelIndex(int channelIndex) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex].getInputGain();
+ }
+ /**
+ * Sets the inputGain value in decibels (dB) for the channel indicated by channelIndex.
+ * @param channelIndex index of channel of interest
+ * @param inputGain desired value in decibels (dB).
+ */
+ public void setInputGainByChannelIndex(int channelIndex, float inputGain) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setInputGain(inputGain);
+ }
+ /**
+ * Sets the inputGain value in decibels (dB) for ALL channels
+ * @param inputGain desired value in decibels (dB)
+ */
+ public void setInputGainAllChannelsTo(float inputGain) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setInputGain(inputGain);
+ }
+ }
+
+ //=== PreEQ
+ /**
+ * Gets PreEq stage from channel indicated by channelIndex
+ * @param channelIndex index of channel of interest
+ * @return PreEq stage configuration object
+ */
+ public Eq getPreEqByChannelIndex(int channelIndex) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex].getPreEq();
+ }
+ /**
+ * Sets the PreEq stage configuration for the channel indicated by channelIndex. Note that
+ * new preEq stage must have the same number of bands than original preEq stage
+ * @param channelIndex index of channel to be set
+ * @param preEq desired PreEq configuration to be set
+ */
+ public void setPreEqByChannelIndex(int channelIndex, Eq preEq) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setPreEq(preEq);
+ }
+ /**
+ * Sets the PreEq stage configuration for ALL channels. Note that new preEq stage must have
+ * the same number of bands than original preEq stages.
+ * @param preEq desired PreEq configuration to be set
+ */
+ public void setPreEqAllChannelsTo(Eq preEq) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setPreEq(preEq);
+ }
+ }
+ public EqBand getPreEqBandByChannelIndex(int channelIndex, int band) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex].getPreEqBand(band);
+ }
+ public void setPreEqBandByChannelIndex(int channelIndex, int band, EqBand preEqBand) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setPreEqBand(band, preEqBand);
+ }
+ public void setPreEqBandAllChannelsTo(int band, EqBand preEqBand) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setPreEqBand(band, preEqBand);
+ }
+ }
+
+ //=== MBC
+ public Mbc getMbcByChannelIndex(int channelIndex) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex].getMbc();
+ }
+ public void setMbcByChannelIndex(int channelIndex, Mbc mbc) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setMbc(mbc);
+ }
+ public void setMbcAllChannelsTo(Mbc mbc) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setMbc(mbc);
+ }
+ }
+ public MbcBand getMbcBandByChannelIndex(int channelIndex, int band) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex].getMbcBand(band);
+ }
+ public void setMbcBandByChannelIndex(int channelIndex, int band, MbcBand mbcBand) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setMbcBand(band, mbcBand);
+ }
+ public void setMbcBandAllChannelsTo(int band, MbcBand mbcBand) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setMbcBand(band, mbcBand);
+ }
+ }
+
+ //=== PostEQ
+ public Eq getPostEqByChannelIndex(int channelIndex) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex].getPostEq();
+ }
+ public void setPostEqByChannelIndex(int channelIndex, Eq postEq) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setPostEq(postEq);
+ }
+ public void setPostEqAllChannelsTo(Eq postEq) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setPostEq(postEq);
+ }
+ }
+ public EqBand getPostEqBandByChannelIndex(int channelIndex, int band) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex].getPostEqBand(band);
+ }
+ public void setPostEqBandByChannelIndex(int channelIndex, int band, EqBand postEqBand) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setPostEqBand(band, postEqBand);
+ }
+ public void setPostEqBandAllChannelsTo(int band, EqBand postEqBand) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setPostEqBand(band, postEqBand);
+ }
+ }
+
+ //Limiter
+ public Limiter getLimiterByChannelIndex(int channelIndex) {
+ checkChannel(channelIndex);
+ return mChannel[channelIndex].getLimiter();
+ }
+ public void setLimiterByChannelIndex(int channelIndex, Limiter limiter) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setLimiter(limiter);
+ }
+ public void setLimiterAllChannelsTo(Limiter limiter) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setLimiter(limiter);
+ }
+ }
+
+ public final static class Builder {
+ private int mVariant;
+ private int mChannelCount;
+ private boolean mPreEqInUse;
+ private int mPreEqBandCount;
+ private boolean mMbcInUse;
+ private int mMbcBandCount;
+ private boolean mPostEqInUse;
+ private int mPostEqBandCount;
+ private boolean mLimiterInUse;
+ private float mPreferredFrameDuration = CONFIG_PREFERRED_FRAME_DURATION_MS;
+ private Channel[] mChannel;
+
+ public Builder(int variant, int channelCount,
+ boolean preEqInUse, int preEqBandCount,
+ boolean mbcInUse, int mbcBandCount,
+ boolean postEqInUse, int postEqBandCount,
+ boolean limiterInUse) {
+ mVariant = variant;
+ mChannelCount = channelCount;
+ mPreEqInUse = preEqInUse;
+ mPreEqBandCount = preEqBandCount;
+ mMbcInUse = mbcInUse;
+ mMbcBandCount = mbcBandCount;
+ mPostEqInUse = postEqInUse;
+ mPostEqBandCount = postEqBandCount;
+ mLimiterInUse = limiterInUse;
+ mChannel = new Channel[mChannelCount];
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ this.mChannel[ch] = new Channel(CHANNEL_DEFAULT_INPUT_GAIN,
+ this.mPreEqInUse, this.mPreEqBandCount,
+ this.mMbcInUse, this.mMbcBandCount,
+ this.mPostEqInUse, this.mPostEqBandCount,
+ this.mLimiterInUse);
+ }
+ }
+
+ private void checkChannel(int channelIndex) {
+ if (channelIndex < 0 || channelIndex >= mChannel.length) {
+ throw new IllegalArgumentException("ChannelIndex out of bounds");
+ }
+ }
+
+ public Builder setPreferredFrameDuration(float frameDuration) {
+ if (frameDuration < 0) {
+ throw new IllegalArgumentException("Expected positive frameDuration");
+ }
+ mPreferredFrameDuration = frameDuration;
+ return this;
+ }
+
+ public Builder setInputGainByChannelIndex(int channelIndex, float inputGain) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setInputGain(inputGain);
+ return this;
+ }
+ public Builder setInputGainAllChannelsTo(float inputGain) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ mChannel[ch].setInputGain(inputGain);
+ }
+ return this;
+ }
+
+ public Builder setChannelTo(int channelIndex, Channel channel) {
+ checkChannel(channelIndex);
+ //check all things are compatible
+ if (mMbcBandCount != channel.getMbc().getBandCount()) {
+ throw new IllegalArgumentException("MbcBandCount changed from " +
+ mMbcBandCount + " to " + channel.getPreEq().getBandCount());
+ }
+ if (mPreEqBandCount != channel.getPreEq().getBandCount()) {
+ throw new IllegalArgumentException("PreEqBandCount changed from " +
+ mPreEqBandCount + " to " + channel.getPreEq().getBandCount());
+ }
+ if (mPostEqBandCount != channel.getPostEq().getBandCount()) {
+ throw new IllegalArgumentException("PostEqBandCount changed from " +
+ mPostEqBandCount + " to " + channel.getPostEq().getBandCount());
+ }
+ mChannel[channelIndex] = new Channel(channel);
+ return this;
+ }
+ public Builder setAllChannelsTo(Channel channel) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ setChannelTo(ch, channel);
+ }
+ return this;
+ }
+
+ public Builder setPreEqByChannelIndex(int channelIndex, Eq preEq) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setPreEq(preEq);
+ return this;
+ }
+ public Builder setPreEqAllChannelsTo(Eq preEq) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ setPreEqByChannelIndex(ch, preEq);
+ }
+ return this;
+ }
+
+ public Builder setMbcByChannelIndex(int channelIndex, Mbc mbc) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setMbc(mbc);
+ return this;
+ }
+ public Builder setMbcAllChannelsTo(Mbc mbc) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ setMbcByChannelIndex(ch, mbc);
+ }
+ return this;
+ }
+
+ public Builder setPostEqByChannelIndex(int channelIndex, Eq postEq) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setPostEq(postEq);
+ return this;
+ }
+ public Builder setPostEqAllChannelsTo(Eq postEq) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ setPostEqByChannelIndex(ch, postEq);
+ }
+ return this;
+ }
+
+ public Builder setLimiterByChannelIndex(int channelIndex, Limiter limiter) {
+ checkChannel(channelIndex);
+ mChannel[channelIndex].setLimiter(limiter);
+ return this;
+ }
+ public Builder setLimiterAllChannelsTo(Limiter limiter) {
+ for (int ch = 0; ch < mChannel.length; ch++) {
+ setLimiterByChannelIndex(ch, limiter);
+ }
+ return this;
+ }
+
+ public Config build() {
+ return new Config(mVariant, mPreferredFrameDuration, mChannelCount,
+ mPreEqInUse, mPreEqBandCount,
+ mMbcInUse, mMbcBandCount,
+ mPostEqInUse, mPostEqBandCount,
+ mLimiterInUse, mChannel);
+ }
+ }
+ }
+ //=== CHANNEL
+ public Channel getChannelByChannelIndex(int channelIndex) {
+ return queryEngineByChannelIndex(channelIndex);
+ }
+
+ public void setChannelTo(int channelIndex, Channel channel) {
+ updateEngineChannelByChannelIndex(channelIndex, channel);
+ }
+
+ public void setAllChannelsTo(Channel channel) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setChannelTo(ch, channel);
+ }
+ }
+
+ //=== channel params
+ public float getInputGainByChannelIndex(int channelIndex) {
+ return getTwoFloat(PARAM_INPUT_GAIN, channelIndex);
+ }
+ public void setInputGainbyChannel(int channelIndex, float inputGain) {
+ setTwoFloat(PARAM_INPUT_GAIN, channelIndex, inputGain);
+ }
+ public void setInputGainAllChannelsTo(float inputGain) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setInputGainbyChannel(ch, inputGain);
+ }
+ }
+
+ //=== PreEQ
+ public Eq getPreEqByChannelIndex(int channelIndex) {
+ return queryEngineEqByChannelIndex(PARAM_PRE_EQ, channelIndex);
+ }
+ public void setPreEqByChannelIndex(int channelIndex, Eq preEq) {
+ updateEngineEqByChannelIndex(PARAM_PRE_EQ, channelIndex, preEq);
+ }
+ public void setPreEqAllChannelsTo(Eq preEq) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setPreEqByChannelIndex(ch, preEq);
+ }
+ }
+ public EqBand getPreEqBandByChannelIndex(int channelIndex, int band) {
+ return queryEngineEqBandByChannelIndex(PARAM_PRE_EQ_BAND, channelIndex, band);
+ }
+ public void setPreEqBandByChannelIndex(int channelIndex, int band, EqBand preEqBand) {
+ updateEngineEqBandByChannelIndex(PARAM_PRE_EQ_BAND, channelIndex, band, preEqBand);
+ }
+ public void setPreEqBandAllChannelsTo(int band, EqBand preEqBand) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setPreEqBandByChannelIndex(ch, band, preEqBand);
+ }
+ }
+
+ //=== MBC
+ public Mbc getMbcByChannelIndex(int channelIndex) {
+ return queryEngineMbcByChannelIndex(channelIndex);
+ }
+ public void setMbcByChannelIndex(int channelIndex, Mbc mbc) {
+ updateEngineMbcByChannelIndex(channelIndex, mbc);
+ }
+ public void setMbcAllChannelsTo(Mbc mbc) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setMbcByChannelIndex(ch, mbc);
+ }
+ }
+ public MbcBand getMbcBandByChannelIndex(int channelIndex, int band) {
+ return queryEngineMbcBandByChannelIndex(channelIndex, band);
+ }
+ public void setMbcBandByChannelIndex(int channelIndex, int band, MbcBand mbcBand) {
+ updateEngineMbcBandByChannelIndex(channelIndex, band, mbcBand);
+ }
+ public void setMbcBandAllChannelsTo(int band, MbcBand mbcBand) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setMbcBandByChannelIndex(ch, band, mbcBand);
+ }
+ }
+
+ //== PostEq
+ public Eq getPostEqByChannelIndex(int channelIndex) {
+ return queryEngineEqByChannelIndex(PARAM_POST_EQ, channelIndex);
+ }
+ public void setPostEqByChannelIndex(int channelIndex, Eq postEq) {
+ updateEngineEqByChannelIndex(PARAM_POST_EQ, channelIndex, postEq);
+ }
+ public void setPostEqAllChannelsTo(Eq postEq) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setPostEqByChannelIndex(ch, postEq);
+ }
+ }
+ public EqBand getPostEqBandByChannelIndex(int channelIndex, int band) {
+ return queryEngineEqBandByChannelIndex(PARAM_POST_EQ_BAND, channelIndex, band);
+ }
+ public void setPostEqBandByChannelIndex(int channelIndex, int band, EqBand postEqBand) {
+ updateEngineEqBandByChannelIndex(PARAM_POST_EQ_BAND, channelIndex, band, postEqBand);
+ }
+ public void setPostEqBandAllChannelsTo(int band, EqBand postEqBand) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setPostEqBandByChannelIndex(ch, band, postEqBand);
+ }
+ }
+
+ //==== Limiter
+ public Limiter getLimiterByChannelIndex(int channelIndex) {
+ return queryEngineLimiterByChannelIndex(channelIndex);
+ }
+ public void setLimiterByChannelIndex(int channelIndex, Limiter limiter) {
+ updateEngineLimiterByChannelIndex(channelIndex, limiter);
+ }
+ public void setLimiterAllChannelsTo(Limiter limiter) {
+ for (int ch = 0; ch < mChannelCount; ch++) {
+ setLimiterByChannelIndex(ch, limiter);
+ }
+ }
+
+ /**
+ * Gets the number of channels in the effect engine
+ * @return number of channels currently in use by the effect engine
+ */
+ public int getChannelCount() {
+ return getOneInt(PARAM_GET_CHANNEL_COUNT);
+ }
+
+ //=== Engine calls
+ private void setEngineArchitecture(int variant, float preferredFrameDuration,
+ boolean preEqInUse, int preEqBandCount, boolean mbcInUse, int mbcBandCount,
+ boolean postEqInUse, int postEqBandCount, boolean limiterInUse) {
+
+ Number[] params = { PARAM_ENGINE_ARCHITECTURE };
+ Number[] values = { variant /* variant */,
+ preferredFrameDuration,
+ (preEqInUse ? 1 : 0),
+ preEqBandCount,
+ (mbcInUse ? 1 : 0),
+ mbcBandCount,
+ (postEqInUse ? 1 : 0),
+ postEqBandCount,
+ (limiterInUse ? 1 : 0)};
+ setNumberArray(params, values);
+ }
+
+ private void updateEngineEqBandByChannelIndex(int param, int channelIndex, int bandIndex,
+ @NonNull EqBand eqBand) {
+ Number[] params = {param,
+ channelIndex,
+ bandIndex};
+ Number[] values = {(eqBand.isEnabled() ? 1 : 0),
+ eqBand.getCutoffFrequency(),
+ eqBand.getGain()};
+ setNumberArray(params, values);
+ }
+ private Eq queryEngineEqByChannelIndex(int param, int channelIndex) {
+
+ Number[] params = {param == PARAM_PRE_EQ ? PARAM_PRE_EQ : PARAM_POST_EQ,
+ channelIndex};
+ Number[] values = {0 /*0 in use */,
+ 0 /*1 enabled*/,
+ 0 /*2 band count */};
+ byte[] paramBytes = numberArrayToByteArray(params);
+ byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+ getParameter(paramBytes, valueBytes);
+ byteArrayToNumberArray(valueBytes, values);
+ int bandCount = values[2].intValue();
+ Eq eq = new Eq(values[0].intValue() > 0 /* in use */,
+ values[1].intValue() > 0 /* enabled */,
+ bandCount /*band count*/);
+ for (int b = 0; b < bandCount; b++) {
+ EqBand eqBand = queryEngineEqBandByChannelIndex(param == PARAM_PRE_EQ ?
+ PARAM_PRE_EQ_BAND : PARAM_POST_EQ_BAND, channelIndex, b);
+ eq.setBand(b, eqBand);
+ }
+ return eq;
+ }
+ private EqBand queryEngineEqBandByChannelIndex(int param, int channelIndex, int bandIndex) {
+ Number[] params = {param,
+ channelIndex,
+ bandIndex};
+ Number[] values = {0 /*0 enabled*/,
+ 0.0f /*1 cutoffFrequency */,
+ 0.0f /*2 gain */};
+
+ byte[] paramBytes = numberArrayToByteArray(params);
+ byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+ getParameter(paramBytes, valueBytes);
+
+ byteArrayToNumberArray(valueBytes, values);
+
+ return new EqBand(values[0].intValue() > 0 /* enabled */,
+ values[1].floatValue() /* cutoffFrequency */,
+ values[2].floatValue() /* gain*/);
+ }
+ private void updateEngineEqByChannelIndex(int param, int channelIndex, @NonNull Eq eq) {
+ int bandCount = eq.getBandCount();
+ Number[] params = {param,
+ channelIndex};
+ Number[] values = { (eq.isInUse() ? 1 : 0),
+ (eq.isEnabled() ? 1 : 0),
+ bandCount};
+ setNumberArray(params, values);
+ for (int b = 0; b < bandCount; b++) {
+ EqBand eqBand = eq.getBand(b);
+ updateEngineEqBandByChannelIndex(param == PARAM_PRE_EQ ?
+ PARAM_PRE_EQ_BAND : PARAM_POST_EQ_BAND, channelIndex, b, eqBand);
+ }
+ }
+
+ private Mbc queryEngineMbcByChannelIndex(int channelIndex) {
+ Number[] params = {PARAM_MBC,
+ channelIndex};
+ Number[] values = {0 /*0 in use */,
+ 0 /*1 enabled*/,
+ 0 /*2 band count */};
+ byte[] paramBytes = numberArrayToByteArray(params);
+ byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+ getParameter(paramBytes, valueBytes);
+ byteArrayToNumberArray(valueBytes, values);
+ int bandCount = values[2].intValue();
+ Mbc mbc = new Mbc(values[0].intValue() > 0 /* in use */,
+ values[1].intValue() > 0 /* enabled */,
+ bandCount /*band count*/);
+ for (int b = 0; b < bandCount; b++) {
+ MbcBand mbcBand = queryEngineMbcBandByChannelIndex(channelIndex, b);
+ mbc.setBand(b, mbcBand);
+ }
+ return mbc;
+ }
+ private MbcBand queryEngineMbcBandByChannelIndex(int channelIndex, int bandIndex) {
+ Number[] params = {PARAM_MBC_BAND,
+ channelIndex,
+ bandIndex};
+ Number[] values = {0 /*0 enabled */,
+ 0.0f /*1 cutoffFrequency */,
+ 0.0f /*2 AttackTime */,
+ 0.0f /*3 ReleaseTime */,
+ 0.0f /*4 Ratio */,
+ 0.0f /*5 Threshold */,
+ 0.0f /*6 KneeWidth */,
+ 0.0f /*7 NoiseGateThreshold */,
+ 0.0f /*8 ExpanderRatio */,
+ 0.0f /*9 PreGain */,
+ 0.0f /*10 PostGain*/};
+
+ byte[] paramBytes = numberArrayToByteArray(params);
+ byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+ getParameter(paramBytes, valueBytes);
+
+ byteArrayToNumberArray(valueBytes, values);
+
+ return new MbcBand(values[0].intValue() > 0 /* enabled */,
+ values[1].floatValue() /* cutoffFrequency */,
+ values[2].floatValue()/*2 AttackTime */,
+ values[3].floatValue()/*3 ReleaseTime */,
+ values[4].floatValue()/*4 Ratio */,
+ values[5].floatValue()/*5 Threshold */,
+ values[6].floatValue()/*6 KneeWidth */,
+ values[7].floatValue()/*7 NoiseGateThreshold */,
+ values[8].floatValue()/*8 ExpanderRatio */,
+ values[9].floatValue()/*9 PreGain */,
+ values[10].floatValue()/*10 PostGain*/);
+ }
+ private void updateEngineMbcBandByChannelIndex(int channelIndex, int bandIndex,
+ @NonNull MbcBand mbcBand) {
+ Number[] params = { PARAM_MBC_BAND,
+ channelIndex,
+ bandIndex};
+ Number[] values = {(mbcBand.isEnabled() ? 1 : 0),
+ mbcBand.getCutoffFrequency(),
+ mbcBand.getAttackTime(),
+ mbcBand.getReleaseTime(),
+ mbcBand.getRatio(),
+ mbcBand.getThreshold(),
+ mbcBand.getKneeWidth(),
+ mbcBand.getNoiseGateThreshold(),
+ mbcBand.getExpanderRatio(),
+ mbcBand.getPreGain(),
+ mbcBand.getPostGain()};
+ setNumberArray(params, values);
+ }
+
+ private void updateEngineMbcByChannelIndex(int channelIndex, @NonNull Mbc mbc) {
+ int bandCount = mbc.getBandCount();
+ Number[] params = { PARAM_MBC,
+ channelIndex};
+ Number[] values = {(mbc.isInUse() ? 1 : 0),
+ (mbc.isEnabled() ? 1 : 0),
+ bandCount};
+ setNumberArray(params, values);
+ for (int b = 0; b < bandCount; b++) {
+ MbcBand mbcBand = mbc.getBand(b);
+ updateEngineMbcBandByChannelIndex(channelIndex, b, mbcBand);
+ }
+ }
+
+ private void updateEngineLimiterByChannelIndex(int channelIndex, @NonNull Limiter limiter) {
+ Number[] params = { PARAM_LIMITER,
+ channelIndex};
+ Number[] values = {(limiter.isInUse() ? 1 : 0),
+ (limiter.isEnabled() ? 1 : 0),
+ limiter.getLinkGroup(),
+ limiter.getAttackTime(),
+ limiter.getReleaseTime(),
+ limiter.getRatio(),
+ limiter.getThreshold(),
+ limiter.getPostGain()};
+ setNumberArray(params, values);
+ }
+
+ private Limiter queryEngineLimiterByChannelIndex(int channelIndex) {
+ Number[] params = {PARAM_LIMITER,
+ channelIndex};
+ Number[] values = {0 /*0 in use (int)*/,
+ 0 /*1 enabled (int)*/,
+ 0 /*2 link group (int)*/,
+ 0.0f /*3 attack time (float)*/,
+ 0.0f /*4 release time (float)*/,
+ 0.0f /*5 ratio (float)*/,
+ 0.0f /*6 threshold (float)*/,
+ 0.0f /*7 post gain(float)*/};
+
+ byte[] paramBytes = numberArrayToByteArray(params);
+ byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+ getParameter(paramBytes, valueBytes);
+ byteArrayToNumberArray(valueBytes, values);
+
+ return new Limiter(values[0].intValue() > 0 /*in use*/,
+ values[1].intValue() > 0 /*enabled*/,
+ values[2].intValue() /*linkGroup*/,
+ values[3].floatValue() /*attackTime*/,
+ values[4].floatValue() /*releaseTime*/,
+ values[5].floatValue() /*ratio*/,
+ values[6].floatValue() /*threshold*/,
+ values[7].floatValue() /*postGain*/);
+ }
+
+ private Channel queryEngineByChannelIndex(int channelIndex) {
+ float inputGain = getTwoFloat(PARAM_INPUT_GAIN, channelIndex);
+ Eq preEq = queryEngineEqByChannelIndex(PARAM_PRE_EQ, channelIndex);
+ Mbc mbc = queryEngineMbcByChannelIndex(channelIndex);
+ Eq postEq = queryEngineEqByChannelIndex(PARAM_POST_EQ, channelIndex);
+ Limiter limiter = queryEngineLimiterByChannelIndex(channelIndex);
+
+ Channel channel = new Channel(inputGain,
+ preEq.isInUse(), preEq.getBandCount(),
+ mbc.isInUse(), mbc.getBandCount(),
+ postEq.isInUse(), postEq.getBandCount(),
+ limiter.isInUse());
+ channel.setInputGain(inputGain);
+ channel.setPreEq(preEq);
+ channel.setMbc(mbc);
+ channel.setPostEq(postEq);
+ channel.setLimiter(limiter);
+ return channel;
+ }
+
+ private void updateEngineChannelByChannelIndex(int channelIndex, @NonNull Channel channel) {
+ //send things with as few calls as possible
+ setTwoFloat(PARAM_INPUT_GAIN, channelIndex, channel.getInputGain());
+ Eq preEq = channel.getPreEq();
+ updateEngineEqByChannelIndex(PARAM_PRE_EQ, channelIndex, preEq);
+ Mbc mbc = channel.getMbc();
+ updateEngineMbcByChannelIndex(channelIndex, mbc);
+ Eq postEq = channel.getPostEq();
+ updateEngineEqByChannelIndex(PARAM_POST_EQ, channelIndex, postEq);
+ Limiter limiter = channel.getLimiter();
+ updateEngineLimiterByChannelIndex(channelIndex, limiter);
+ }
+
+ //****** convenience methods:
+ //
+ private int getOneInt(int param) {
+ final int[] params = { param };
+ final int[] result = new int[1];
+
+ checkStatus(getParameter(params, result));
+ return result[0];
+ }
+
+ private void setTwoFloat(int param, int paramA, float valueSet) {
+ final int[] params = { param, paramA };
+ final byte[] value;
+
+ value = floatToByteArray(valueSet);
+ checkStatus(setParameter(params, value));
+ }
+
+ private byte[] numberArrayToByteArray(Number[] values) {
+ int expectedBytes = 0;
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] instanceof Integer) {
+ expectedBytes += Integer.BYTES;
+ } else if (values[i] instanceof Float) {
+ expectedBytes += Float.BYTES;
+ } else {
+ throw new IllegalArgumentException("unknown value type " +
+ values[i].getClass());
+ }
+ }
+ ByteBuffer converter = ByteBuffer.allocate(expectedBytes);
+ converter.order(ByteOrder.nativeOrder());
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] instanceof Integer) {
+ converter.putInt(values[i].intValue());
+ } else if (values[i] instanceof Float) {
+ converter.putFloat(values[i].floatValue());
+ }
+ }
+ return converter.array();
+ }
+
+ private void byteArrayToNumberArray(byte[] valuesIn, Number[] valuesOut) {
+ int inIndex = 0;
+ int outIndex = 0;
+ while (inIndex < valuesIn.length && outIndex < valuesOut.length) {
+ if (valuesOut[outIndex] instanceof Integer) {
+ valuesOut[outIndex++] = byteArrayToInt(valuesIn, inIndex);
+ inIndex += Integer.BYTES;
+ } else if (valuesOut[outIndex] instanceof Float) {
+ valuesOut[outIndex++] = byteArrayToFloat(valuesIn, inIndex);
+ inIndex += Float.BYTES;
+ } else {
+ throw new IllegalArgumentException("can't convert " +
+ valuesOut[outIndex].getClass());
+ }
+ }
+ if (outIndex != valuesOut.length) {
+ throw new IllegalArgumentException("only converted " + outIndex +
+ " values out of "+ valuesOut.length + " expected");
+ }
+ }
+
+ private void setNumberArray(Number[] params, Number[] values) {
+ byte[] paramBytes = numberArrayToByteArray(params);
+ byte[] valueBytes = numberArrayToByteArray(values);
+ checkStatus(setParameter(paramBytes, valueBytes));
+ }
+
+ private float getTwoFloat(int param, int paramA) {
+ final int[] params = { param, paramA };
+ final byte[] result = new byte[4];
+
+ checkStatus(getParameter(params, result));
+ return byteArrayToFloat(result);
+ }
+
+ /**
+ * @hide
+ * The OnParameterChangeListener interface defines a method called by the DynamicsProcessing
+ * when a parameter value has changed.
+ */
+ public interface OnParameterChangeListener {
+ /**
+ * Method called when a parameter value has changed. The method is called only if the
+ * parameter was changed by another application having the control of the same
+ * DynamicsProcessing engine.
+ * @param effect the DynamicsProcessing on which the interface is registered.
+ * @param param ID of the modified parameter. See {@link #PARAM_GENERIC_PARAM1} ...
+ * @param value the new parameter value.
+ */
+ void onParameterChange(DynamicsProcessing effect, int param, int value);
+ }
+
+ /**
+ * helper method to update effect architecture parameters
+ */
+ private void updateEffectArchitecture() {
+ mChannelCount = getChannelCount();
+ }
+
+ /**
+ * Listener used internally to receive unformatted parameter change events from AudioEffect
+ * super class.
+ */
+ private class BaseParameterListener implements AudioEffect.OnParameterChangeListener {
+ private BaseParameterListener() {
+
+ }
+ public void onParameterChange(AudioEffect effect, int status, byte[] param, byte[] value) {
+ // only notify when the parameter was successfully change
+ if (status != AudioEffect.SUCCESS) {
+ return;
+ }
+ OnParameterChangeListener l = null;
+ synchronized (mParamListenerLock) {
+ if (mParamListener != null) {
+ l = mParamListener;
+ }
+ }
+ if (l != null) {
+ int p = -1;
+ int v = Integer.MIN_VALUE;
+
+ if (param.length == 4) {
+ p = byteArrayToInt(param, 0);
+ }
+ if (value.length == 4) {
+ v = byteArrayToInt(value, 0);
+ }
+ if (p != -1 && v != Integer.MIN_VALUE) {
+ l.onParameterChange(DynamicsProcessing.this, p, v);
+ }
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * Registers an OnParameterChangeListener interface.
+ * @param listener OnParameterChangeListener interface registered
+ */
+ public void setParameterListener(OnParameterChangeListener listener) {
+ synchronized (mParamListenerLock) {
+ if (mParamListener == null) {
+ mBaseParamListener = new BaseParameterListener();
+ super.setParameterListener(mBaseParamListener);
+ }
+ mParamListener = listener;
+ }
+ }
+
+ /**
+ * @hide
+ * The Settings class regroups the DynamicsProcessing parameters. It is used in
+ * conjunction with the getProperties() and setProperties() methods to backup and restore
+ * all parameters in a single call.
+ */
+
+ public static class Settings {
+ public int channelCount;
+ public float[] inputGain;
+
+ public Settings() {
+ }
+
+ /**
+ * Settings class constructor from a key=value; pairs formatted string. The string is
+ * typically returned by Settings.toString() method.
+ * @throws IllegalArgumentException if the string is not correctly formatted.
+ */
+ public Settings(String settings) {
+ StringTokenizer st = new StringTokenizer(settings, "=;");
+ //int tokens = st.countTokens();
+ if (st.countTokens() != 3) {
+ throw new IllegalArgumentException("settings: " + settings);
+ }
+ String key = st.nextToken();
+ if (!key.equals("DynamicsProcessing")) {
+ throw new IllegalArgumentException(
+ "invalid settings for DynamicsProcessing: " + key);
+ }
+ try {
+ key = st.nextToken();
+ if (!key.equals("channelCount")) {
+ throw new IllegalArgumentException("invalid key name: " + key);
+ }
+ channelCount = Short.parseShort(st.nextToken());
+ if (channelCount > CHANNEL_COUNT_MAX) {
+ throw new IllegalArgumentException("too many channels Settings:" + settings);
+ }
+ if (st.countTokens() != channelCount*1) { //check expected parameters.
+ throw new IllegalArgumentException("settings: " + settings);
+ }
+ //check to see it is ok the size
+ inputGain = new float[channelCount];
+ for (int ch = 0; ch < channelCount; ch++) {
+ key = st.nextToken();
+ if (!key.equals(ch +"_inputGain")) {
+ throw new IllegalArgumentException("invalid key name: " + key);
+ }
+ inputGain[ch] = Float.parseFloat(st.nextToken());
+ }
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException("invalid value for key: " + key);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String str = new String (
+ "DynamicsProcessing"+
+ ";channelCount="+Integer.toString(channelCount));
+ for (int ch = 0; ch < channelCount; ch++) {
+ str = str.concat(";"+ch+"_inputGain="+Float.toString(inputGain[ch]));
+ }
+ return str;
+ }
+ };
+
+
+ /**
+ * @hide
+ * Gets the DynamicsProcessing properties. This method is useful when a snapshot of current
+ * effect settings must be saved by the application.
+ * @return a DynamicsProcessing.Settings object containing all current parameters values
+ */
+ public DynamicsProcessing.Settings getProperties() {
+ Settings settings = new Settings();
+
+ //TODO: just for testing, we are calling the getters one by one, this is
+ // supposed to be done in a single (or few calls) and get all the parameters at once.
+
+ settings.channelCount = getChannelCount();
+
+ if (settings.channelCount > CHANNEL_COUNT_MAX) {
+ throw new IllegalArgumentException("too many channels Settings:" + settings);
+ }
+
+ { // get inputGainmB per channel
+ settings.inputGain = new float [settings.channelCount];
+ for (int ch = 0; ch < settings.channelCount; ch++) {
+//TODO:with config settings.inputGain[ch] = getInputGain(ch);
+ }
+ }
+ return settings;
+ }
+
+ /**
+ * @hide
+ * Sets the DynamicsProcessing properties. This method is useful when bass boost settings
+ * have to be applied from a previous backup.
+ * @param settings a DynamicsProcessing.Settings object containing the properties to apply
+ */
+ public void setProperties(DynamicsProcessing.Settings settings) {
+
+ if (settings.channelCount != settings.inputGain.length ||
+ settings.channelCount != mChannelCount) {
+ throw new IllegalArgumentException("settings invalid channel count: "
+ + settings.channelCount);
+ }
+
+ //TODO: for now calling multiple times.
+ for (int ch = 0; ch < mChannelCount; ch++) {
+//TODO: use config setInputGain(ch, settings.inputGain[ch]);
+ }
+ }
+}
diff --git a/android/media/audiofx/Visualizer.java b/android/media/audiofx/Visualizer.java
index 0fe7246e..f2b4fe09 100644
--- a/android/media/audiofx/Visualizer.java
+++ b/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/android/media/audiopolicy/AudioMix.java b/android/media/audiopolicy/AudioMix.java
index adeb8348..fca0cc73 100644
--- a/android/media/audiopolicy/AudioMix.java
+++ b/android/media/audiopolicy/AudioMix.java
@@ -162,6 +162,24 @@ public class AudioMix {
}
/** @hide */
+ public boolean isAffectingUsage(int usage) {
+ return mRule.isAffectingUsage(usage);
+ }
+
+ /** @hide */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final AudioMix that = (AudioMix) o;
+ return (this.mRouteFlags == that.mRouteFlags)
+ && (this.mRule == that.mRule)
+ && (this.mMixType == that.mMixType)
+ && (this.mFormat == that.mFormat);
+ }
+
+ /** @hide */
@Override
public int hashCode() {
return Objects.hash(mRouteFlags, mRule, mMixType, mFormat);
diff --git a/android/media/audiopolicy/AudioMixingRule.java b/android/media/audiopolicy/AudioMixingRule.java
index 5f127421..749a45e3 100644
--- a/android/media/audiopolicy/AudioMixingRule.java
+++ b/android/media/audiopolicy/AudioMixingRule.java
@@ -135,11 +135,42 @@ public class AudioMixingRule {
}
}
+ boolean isAffectingUsage(int usage) {
+ for (AudioMixMatchCriterion criterion : mCriteria) {
+ if ((criterion.mRule & RULE_MATCH_ATTRIBUTE_USAGE) != 0
+ && criterion.mAttr != null
+ && criterion.mAttr.getUsage() == usage) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean areCriteriaEquivalent(ArrayList<AudioMixMatchCriterion> cr1,
+ ArrayList<AudioMixMatchCriterion> cr2) {
+ if (cr1 == null || cr2 == null) return false;
+ if (cr1 == cr2) return true;
+ if (cr1.size() != cr2.size()) return false;
+ //TODO iterate over rules to check they contain the same criterion
+ return (cr1.hashCode() == cr2.hashCode());
+ }
+
private final int mTargetMixType;
int getTargetMixType() { return mTargetMixType; }
private final ArrayList<AudioMixMatchCriterion> mCriteria;
ArrayList<AudioMixMatchCriterion> getCriteria() { return mCriteria; }
+ /** @hide */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final AudioMixingRule that = (AudioMixingRule) o;
+ return (this.mTargetMixType == that.mTargetMixType)
+ && (areCriteriaEquivalent(this.mCriteria, that.mCriteria));
+ }
+
@Override
public int hashCode() {
return Objects.hash(mTargetMixType, mCriteria);
diff --git a/android/media/audiopolicy/AudioPolicy.java b/android/media/audiopolicy/AudioPolicy.java
index 7e88c277..11107e2d 100644
--- a/android/media/audiopolicy/AudioPolicy.java
+++ b/android/media/audiopolicy/AudioPolicy.java
@@ -42,6 +42,7 @@ import android.util.Slog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.List;
/**
* @hide
@@ -89,6 +90,8 @@ public class AudioPolicy {
private AudioPolicyFocusListener mFocusListener;
+ private final AudioPolicyVolumeCallback mVolCb;
+
private Context mContext;
private AudioPolicyConfig mConfig;
@@ -99,12 +102,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 +126,7 @@ public class AudioPolicy {
mFocusListener = fl;
mStatusListener = sl;
mIsFocusPolicy = isFocusPolicy;
+ mVolCb = vc;
}
/**
@@ -134,6 +141,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 +216,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 +253,90 @@ public class AudioPolicy {
+ "an AudioPolicyFocusListener");
}
return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext, mLooper,
- mFocusListener, mStatusListener, mIsFocusPolicy);
+ mFocusListener, mStatusListener, mIsFocusPolicy, mVolCb);
+ }
+ }
+
+ /**
+ * @hide
+ * Update the current configuration of the set of audio mixes by adding new ones, while
+ * keeping the policy registered.
+ * This method can only be called on a registered policy.
+ * @param mixes the list of {@link AudioMix} to add
+ * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR}
+ * otherwise.
+ */
+ @SystemApi
+ public int attachMixes(@NonNull List<AudioMix> mixes) {
+ if (mixes == null) {
+ throw new IllegalArgumentException("Illegal null list of AudioMix");
+ }
+ synchronized (mLock) {
+ if (mStatus != POLICY_STATUS_REGISTERED) {
+ throw new IllegalStateException("Cannot alter unregistered AudioPolicy");
+ }
+ final ArrayList<AudioMix> zeMixes = new ArrayList<AudioMix>(mixes.size());
+ for (AudioMix mix : mixes) {
+ if (mix == null) {
+ throw new IllegalArgumentException("Illegal null AudioMix in attachMixes");
+ } else {
+ zeMixes.add(mix);
+ }
+ }
+ final AudioPolicyConfig cfg = new AudioPolicyConfig(zeMixes);
+ IAudioService service = getService();
+ try {
+ final int status = service.addMixForPolicy(cfg, this.cb());
+ if (status == AudioManager.SUCCESS) {
+ mConfig.add(zeMixes);
+ }
+ return status;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in attachMixes", e);
+ return AudioManager.ERROR;
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * Update the current configuration of the set of audio mixes by removing some, while
+ * keeping the policy registered.
+ * This method can only be called on a registered policy.
+ * @param mixes the list of {@link AudioMix} to remove
+ * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR}
+ * otherwise.
+ */
+ @SystemApi
+ public int detachMixes(@NonNull List<AudioMix> mixes) {
+ if (mixes == null) {
+ throw new IllegalArgumentException("Illegal null list of AudioMix");
+ }
+ synchronized (mLock) {
+ if (mStatus != POLICY_STATUS_REGISTERED) {
+ throw new IllegalStateException("Cannot alter unregistered AudioPolicy");
+ }
+ final ArrayList<AudioMix> zeMixes = new ArrayList<AudioMix>(mixes.size());
+ for (AudioMix mix : mixes) {
+ if (mix == null) {
+ throw new IllegalArgumentException("Illegal null AudioMix in detachMixes");
+ // TODO also check mix is currently contained in list of mixes
+ } else {
+ zeMixes.add(mix);
+ }
+ }
+ final AudioPolicyConfig cfg = new AudioPolicyConfig(zeMixes);
+ IAudioService service = getService();
+ try {
+ final int status = service.removeMixForPolicy(cfg, this.cb());
+ if (status == AudioManager.SUCCESS) {
+ mConfig.remove(zeMixes);
+ }
+ return status;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in detachMixes", e);
+ return AudioManager.ERROR;
+ }
}
}
@@ -377,6 +484,7 @@ public class AudioPolicy {
new AudioAttributes.Builder()
.setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX)
.addTag(addressForTag(mix))
+ .addTag(AudioRecord.SUBMIX_FIXED_VOLUME)
.build(),
mixFormat,
AudioRecord.getMinBufferSize(mix.getFormat().getSampleRate(),
@@ -440,9 +548,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 +563,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 +619,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 +642,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 +660,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 +704,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/android/media/audiopolicy/AudioPolicyConfig.java b/android/media/audiopolicy/AudioPolicyConfig.java
index cafa5a8c..f725cacf 100644
--- a/android/media/audiopolicy/AudioPolicyConfig.java
+++ b/android/media/audiopolicy/AudioPolicyConfig.java
@@ -16,6 +16,7 @@
package android.media.audiopolicy;
+import android.annotation.NonNull;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioPatch;
@@ -24,6 +25,8 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
import java.util.ArrayList;
import java.util.Objects;
@@ -35,11 +38,16 @@ public class AudioPolicyConfig implements Parcelable {
private static final String TAG = "AudioPolicyConfig";
- protected ArrayList<AudioMix> mMixes;
+ protected final ArrayList<AudioMix> mMixes;
protected int mDuckingPolicy = AudioPolicy.FOCUS_POLICY_DUCKING_IN_APP;
private String mRegistrationId = null;
+ /** counter for the mixes that are / have been in the list of AudioMix
+ * e.g. register 4 mixes (counter is 3), remove 1 (counter is 3), add 1 (counter is 4)
+ */
+ private int mMixCounter = 0;
+
protected AudioPolicyConfig(AudioPolicyConfig conf) {
mMixes = conf.mMixes;
}
@@ -201,20 +209,39 @@ public class AudioPolicyConfig implements Parcelable {
return;
}
mRegistrationId = regId == null ? "" : regId;
- int mixIndex = 0;
for (AudioMix mix : mMixes) {
- if (!mRegistrationId.isEmpty()) {
- if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) ==
- AudioMix.ROUTE_FLAG_LOOP_BACK) {
- mix.setRegistration(mRegistrationId + "mix" + mixTypeId(mix.getMixType()) + ":"
- + mixIndex++);
- } else if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_RENDER) ==
- AudioMix.ROUTE_FLAG_RENDER) {
- mix.setRegistration(mix.mDeviceAddress);
- }
- } else {
- mix.setRegistration("");
+ setMixRegistration(mix);
+ }
+ }
+
+ private void setMixRegistration(@NonNull final AudioMix mix) {
+ if (!mRegistrationId.isEmpty()) {
+ if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) ==
+ AudioMix.ROUTE_FLAG_LOOP_BACK) {
+ mix.setRegistration(mRegistrationId + "mix" + mixTypeId(mix.getMixType()) + ":"
+ + mMixCounter);
+ } else if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_RENDER) ==
+ AudioMix.ROUTE_FLAG_RENDER) {
+ mix.setRegistration(mix.mDeviceAddress);
}
+ } else {
+ mix.setRegistration("");
+ }
+ mMixCounter++;
+ }
+
+ @GuardedBy("mMixes")
+ protected void add(@NonNull ArrayList<AudioMix> mixes) {
+ for (AudioMix mix : mixes) {
+ setMixRegistration(mix);
+ mMixes.add(mix);
+ }
+ }
+
+ @GuardedBy("mMixes")
+ protected void remove(@NonNull ArrayList<AudioMix> mixes) {
+ for (AudioMix mix : mixes) {
+ mMixes.remove(mix);
}
}
diff --git a/android/media/midi/MidiManager.java b/android/media/midi/MidiManager.java
index a015732d..dee94c68 100644
--- a/android/media/midi/MidiManager.java
+++ b/android/media/midi/MidiManager.java
@@ -16,9 +16,11 @@
package android.media.midi;
+import android.annotation.RequiresFeature;
import android.annotation.SystemService;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.IBinder;
import android.os.Bundle;
@@ -32,6 +34,7 @@ import java.util.concurrent.ConcurrentHashMap;
* This class is the public application interface to the MIDI service.
*/
@SystemService(Context.MIDI_SERVICE)
+@RequiresFeature(PackageManager.FEATURE_MIDI)
public final class MidiManager {
private static final String TAG = "MidiManager";
diff --git a/android/media/session/MediaController.java b/android/media/session/MediaController.java
index 622900f5..f16804c9 100644
--- a/android/media/session/MediaController.java
+++ b/android/media/session/MediaController.java
@@ -133,7 +133,7 @@ public final class MediaController {
return false;
}
try {
- return mSessionBinder.sendMediaButton(keyEvent);
+ return mSessionBinder.sendMediaButton(mContext.getPackageName(), keyEvent);
} catch (RemoteException e) {
// System is dead. =(
}
@@ -301,7 +301,7 @@ public final class MediaController {
*/
public void setVolumeTo(int value, int flags) {
try {
- mSessionBinder.setVolumeTo(value, flags, mContext.getPackageName());
+ mSessionBinder.setVolumeTo(mContext.getPackageName(), value, flags);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling setVolumeTo.", e);
}
@@ -322,7 +322,7 @@ public final class MediaController {
*/
public void adjustVolume(int direction, int flags) {
try {
- mSessionBinder.adjustVolume(direction, flags, mContext.getPackageName());
+ mSessionBinder.adjustVolume(mContext.getPackageName(), direction, flags);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling adjustVolumeBy.", e);
}
@@ -388,7 +388,7 @@ public final class MediaController {
throw new IllegalArgumentException("command cannot be null or empty");
}
try {
- mSessionBinder.sendCommand(command, args, cb);
+ mSessionBinder.sendCommand(mContext.getPackageName(), command, args, cb);
} catch (RemoteException e) {
Log.d(TAG, "Dead object in sendCommand.", e);
}
@@ -600,7 +600,7 @@ public final class MediaController {
*/
public void prepare() {
try {
- mSessionBinder.prepare();
+ mSessionBinder.prepare(mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling prepare.", e);
}
@@ -624,7 +624,7 @@ public final class MediaController {
"You must specify a non-empty String for prepareFromMediaId.");
}
try {
- mSessionBinder.prepareFromMediaId(mediaId, extras);
+ mSessionBinder.prepareFromMediaId(mContext.getPackageName(), mediaId, extras);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e);
}
@@ -650,7 +650,7 @@ public final class MediaController {
query = "";
}
try {
- mSessionBinder.prepareFromSearch(query, extras);
+ mSessionBinder.prepareFromSearch(mContext.getPackageName(), query, extras);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling prepare(" + query + ").", e);
}
@@ -674,7 +674,7 @@ public final class MediaController {
"You must specify a non-empty Uri for prepareFromUri.");
}
try {
- mSessionBinder.prepareFromUri(uri, extras);
+ mSessionBinder.prepareFromUri(mContext.getPackageName(), uri, extras);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling prepare(" + uri + ").", e);
}
@@ -685,7 +685,7 @@ public final class MediaController {
*/
public void play() {
try {
- mSessionBinder.play();
+ mSessionBinder.play(mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling play.", e);
}
@@ -704,7 +704,7 @@ public final class MediaController {
"You must specify a non-empty String for playFromMediaId.");
}
try {
- mSessionBinder.playFromMediaId(mediaId, extras);
+ mSessionBinder.playFromMediaId(mContext.getPackageName(), mediaId, extras);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling play(" + mediaId + ").", e);
}
@@ -726,7 +726,7 @@ public final class MediaController {
query = "";
}
try {
- mSessionBinder.playFromSearch(query, extras);
+ mSessionBinder.playFromSearch(mContext.getPackageName(), query, extras);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling play(" + query + ").", e);
}
@@ -745,7 +745,7 @@ public final class MediaController {
"You must specify a non-empty Uri for playFromUri.");
}
try {
- mSessionBinder.playFromUri(uri, extras);
+ mSessionBinder.playFromUri(mContext.getPackageName(), uri, extras);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling play(" + uri + ").", e);
}
@@ -757,7 +757,7 @@ public final class MediaController {
*/
public void skipToQueueItem(long id) {
try {
- mSessionBinder.skipToQueueItem(id);
+ mSessionBinder.skipToQueueItem(mContext.getPackageName(), id);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
}
@@ -769,7 +769,7 @@ public final class MediaController {
*/
public void pause() {
try {
- mSessionBinder.pause();
+ mSessionBinder.pause(mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling pause.", e);
}
@@ -781,7 +781,7 @@ public final class MediaController {
*/
public void stop() {
try {
- mSessionBinder.stop();
+ mSessionBinder.stop(mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling stop.", e);
}
@@ -794,7 +794,7 @@ public final class MediaController {
*/
public void seekTo(long pos) {
try {
- mSessionBinder.seekTo(pos);
+ mSessionBinder.seekTo(mContext.getPackageName(), pos);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling seekTo.", e);
}
@@ -806,7 +806,7 @@ public final class MediaController {
*/
public void fastForward() {
try {
- mSessionBinder.fastForward();
+ mSessionBinder.fastForward(mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling fastForward.", e);
}
@@ -817,7 +817,7 @@ public final class MediaController {
*/
public void skipToNext() {
try {
- mSessionBinder.next();
+ mSessionBinder.next(mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling next.", e);
}
@@ -829,7 +829,7 @@ public final class MediaController {
*/
public void rewind() {
try {
- mSessionBinder.rewind();
+ mSessionBinder.rewind(mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling rewind.", e);
}
@@ -840,7 +840,7 @@ public final class MediaController {
*/
public void skipToPrevious() {
try {
- mSessionBinder.previous();
+ mSessionBinder.previous(mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling previous.", e);
}
@@ -855,7 +855,7 @@ public final class MediaController {
*/
public void setRating(Rating rating) {
try {
- mSessionBinder.rate(rating);
+ mSessionBinder.rate(mContext.getPackageName(), rating);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling rate.", e);
}
@@ -890,7 +890,7 @@ public final class MediaController {
throw new IllegalArgumentException("CustomAction cannot be null.");
}
try {
- mSessionBinder.sendCustomAction(action, args);
+ mSessionBinder.sendCustomAction(mContext.getPackageName(), action, args);
} catch (RemoteException e) {
Log.d(TAG, "Dead object in sendCustomAction.", e);
}
diff --git a/android/media/session/MediaSession.java b/android/media/session/MediaSession.java
index b8184a07..5e8b8caf 100644
--- a/android/media/session/MediaSession.java
+++ b/android/media/session/MediaSession.java
@@ -39,6 +39,7 @@ import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.UserHandle;
+import android.media.session.MediaSessionManager.RemoteUserInfo;
import android.service.media.MediaBrowserService;
import android.text.TextUtils;
import android.util.Log;
@@ -103,6 +104,16 @@ public final class MediaSession {
*/
public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16;
+ /**
+ * @hide
+ */
+ public static final int INVALID_UID = -1;
+
+ /**
+ * @hide
+ */
+ public static final int INVALID_PID = -1;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {
@@ -119,7 +130,8 @@ public final class MediaSession {
private final ISession mBinder;
private final CallbackStub mCbStub;
- private CallbackMessageHandler mCallbackHandler;
+ // Do not change the name of mCallback. Support lib accesses this by using reflection.
+ private CallbackMessageHandler mCallback;
private VolumeProvider mVolumeProvider;
private PlaybackState mPlaybackState;
@@ -194,13 +206,13 @@ public final class MediaSession {
*/
public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
synchronized (mLock) {
- if (mCallbackHandler != null) {
+ if (mCallback != null) {
// We're updating the callback, clear the session from the old one.
- mCallbackHandler.mCallback.mSession = null;
- mCallbackHandler.removeCallbacksAndMessages(null);
+ mCallback.mCallback.mSession = null;
+ mCallback.removeCallbacksAndMessages(null);
}
if (callback == null) {
- mCallbackHandler = null;
+ mCallback = null;
return;
}
if (handler == null) {
@@ -209,7 +221,7 @@ public final class MediaSession {
callback.mSession = this;
CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
callback);
- mCallbackHandler = msgHandler;
+ mCallback = msgHandler;
}
}
@@ -500,6 +512,22 @@ public final class MediaSession {
}
/**
+ * Gets the controller information who sent the current request.
+ * <p>
+ * Note: This is only valid while in a request callback, such as {@link Callback#onPlay}.
+ *
+ * @throws IllegalStateException If this method is called outside of {@link Callback} methods.
+ * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
+ */
+ public final @NonNull RemoteUserInfo getCurrentControllerInfo() {
+ if (mCallback == null || mCallback.mCurrentControllerInfo == null) {
+ throw new IllegalStateException(
+ "This should be called inside of MediaSession.Callback methods");
+ }
+ return mCallback.mCurrentControllerInfo;
+ }
+
+ /**
* Notify the system that the remote volume changed.
*
* @param provider The provider that is handling volume changes.
@@ -527,16 +555,14 @@ public final class MediaSession {
* @hide
*/
public String getCallingPackage() {
- try {
- return mBinder.getCallingPackage();
- } catch (RemoteException e) {
- Log.wtf(TAG, "Dead object in getCallingPackage.", e);
+ if (mCallback != null) {
+ return mCallback.mCurrentControllerInfo.getPackageName();
}
return null;
}
- private void dispatchPrepare() {
- postToCallback(CallbackMessageHandler.MSG_PREPARE);
+ private void dispatchPrepare(Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_PREPARE, null, extras);
}
private void dispatchPrepareFromMediaId(String mediaId, Bundle extras) {
@@ -551,8 +577,8 @@ public final class MediaSession {
postToCallback(CallbackMessageHandler.MSG_PREPARE_URI, uri, extras);
}
- private void dispatchPlay() {
- postToCallback(CallbackMessageHandler.MSG_PLAY);
+ private void dispatchPlay(Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_PLAY, null, extras);
}
private void dispatchPlayFromMediaId(String mediaId, Bundle extras) {
@@ -567,75 +593,67 @@ public final class MediaSession {
postToCallback(CallbackMessageHandler.MSG_PLAY_URI, uri, extras);
}
- private void dispatchSkipToItem(long id) {
- postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id);
+ private void dispatchSkipToItem(long id, Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id, extras);
}
- private void dispatchPause() {
- postToCallback(CallbackMessageHandler.MSG_PAUSE);
+ private void dispatchPause(Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_PAUSE, null, extras);
}
- private void dispatchStop() {
- postToCallback(CallbackMessageHandler.MSG_STOP);
+ private void dispatchStop(Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_STOP, null, extras);
}
- private void dispatchNext() {
- postToCallback(CallbackMessageHandler.MSG_NEXT);
+ private void dispatchNext(Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_NEXT, null, extras);
}
- private void dispatchPrevious() {
- postToCallback(CallbackMessageHandler.MSG_PREVIOUS);
+ private void dispatchPrevious(Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_PREVIOUS, null, extras);
}
- private void dispatchFastForward() {
- postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD);
+ private void dispatchFastForward(Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD, null, extras);
}
- private void dispatchRewind() {
- postToCallback(CallbackMessageHandler.MSG_REWIND);
+ private void dispatchRewind(Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_REWIND, null, extras);
}
- private void dispatchSeekTo(long pos) {
- postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos);
+ private void dispatchSeekTo(long pos, Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos, extras);
}
- private void dispatchRate(Rating rating) {
- postToCallback(CallbackMessageHandler.MSG_RATE, rating);
+ private void dispatchRate(Rating rating, Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_RATE, rating, extras);
}
- private void dispatchCustomAction(String action, Bundle args) {
- postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
+ private void dispatchCustomAction(String action, Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, extras);
}
- private void dispatchMediaButton(Intent mediaButtonIntent) {
- postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
+ private void dispatchMediaButton(Intent mediaButtonIntent, Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent, extras);
}
- private void dispatchAdjustVolume(int direction) {
- postToCallback(CallbackMessageHandler.MSG_ADJUST_VOLUME, direction);
+ private void dispatchAdjustVolume(int direction, Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_ADJUST_VOLUME, direction, extras);
}
- private void dispatchSetVolumeTo(int volume) {
- postToCallback(CallbackMessageHandler.MSG_SET_VOLUME, volume);
+ private void dispatchSetVolumeTo(int volume, Bundle extras) {
+ postToCallback(CallbackMessageHandler.MSG_SET_VOLUME, volume, extras);
}
- private void postToCallback(int what) {
- postToCallback(what, null);
- }
-
- private void postCommand(String command, Bundle args, ResultReceiver resultCb) {
+ private void postCommand(String command, Bundle args, ResultReceiver resultCb, Bundle extras) {
Command cmd = new Command(command, args, resultCb);
- postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd);
- }
-
- private void postToCallback(int what, Object obj) {
- postToCallback(what, obj, null);
+ postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd, extras);
}
private void postToCallback(int what, Object obj, Bundle extras) {
synchronized (mLock) {
- if (mCallbackHandler != null) {
- mCallbackHandler.post(what, obj, extras);
+ if (mCallback != null) {
+ mCallback.post(what, obj, extras);
}
}
}
@@ -733,9 +751,13 @@ public final class MediaSession {
* and the system. A callback may be set using {@link #setCallback}.
*/
public abstract static class Callback {
+
private MediaSession mSession;
private CallbackMessageHandler mHandler;
private boolean mMediaPlayPauseKeyPending;
+ private String mCallingPackage;
+ private int mCallingPid;
+ private int mCallingUid;
public Callback() {
}
@@ -1022,24 +1044,26 @@ public final class MediaSession {
private WeakReference<MediaSession> mMediaSession;
public CallbackStub(MediaSession session) {
- mMediaSession = new WeakReference<MediaSession>(session);
+ mMediaSession = new WeakReference<>(session);
}
@Override
- public void onCommand(String command, Bundle args, ResultReceiver cb) {
+ public void onCommand(String packageName, int pid, int uid, String command, Bundle args,
+ ResultReceiver cb) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.postCommand(command, args, cb);
+ session.postCommand(command, args, cb, createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber,
- ResultReceiver cb) {
+ public void onMediaButton(String packageName, int pid, int uid, Intent mediaButtonIntent,
+ int sequenceNumber, ResultReceiver cb) {
MediaSession session = mMediaSession.get();
try {
if (session != null) {
- session.dispatchMediaButton(mediaButtonIntent);
+ session.dispatchMediaButton(
+ mediaButtonIntent, createExtraBundle(packageName, pid, uid));
}
} finally {
if (cb != null) {
@@ -1049,165 +1073,191 @@ public final class MediaSession {
}
@Override
- public void onPrepare() {
+ public void onPrepare(String packageName, int pid, int uid) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPrepare();
+ session.dispatchPrepare(createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+ public void onPrepareFromMediaId(String packageName, int pid, int uid, String mediaId,
+ Bundle extras) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPrepareFromMediaId(mediaId, extras);
+ session.dispatchPrepareFromMediaId(
+ mediaId, createExtraBundle(packageName, pid, uid, extras));
}
}
@Override
- public void onPrepareFromSearch(String query, Bundle extras) {
+ public void onPrepareFromSearch(String packageName, int pid, int uid, String query,
+ Bundle extras) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPrepareFromSearch(query, extras);
+ session.dispatchPrepareFromSearch(
+ query, createExtraBundle(packageName, pid, uid, extras));
}
}
@Override
- public void onPrepareFromUri(Uri uri, Bundle extras) {
+ public void onPrepareFromUri(String packageName, int pid, int uid, Uri uri, Bundle extras) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPrepareFromUri(uri, extras);
+ session.dispatchPrepareFromUri(uri,
+ createExtraBundle(packageName, pid, uid, extras));
}
}
@Override
- public void onPlay() {
+ public void onPlay(String packageName, int pid, int uid) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPlay();
+ session.dispatchPlay(createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ public void onPlayFromMediaId(String packageName, int pid, int uid, String mediaId,
+ Bundle extras) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPlayFromMediaId(mediaId, extras);
+ session.dispatchPlayFromMediaId(
+ mediaId, createExtraBundle(packageName, pid, uid, extras));
}
}
@Override
- public void onPlayFromSearch(String query, Bundle extras) {
+ public void onPlayFromSearch(String packageName, int pid, int uid, String query,
+ Bundle extras) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPlayFromSearch(query, extras);
+ session.dispatchPlayFromSearch(query, createExtraBundle(packageName, pid, uid,
+ extras));
}
}
@Override
- public void onPlayFromUri(Uri uri, Bundle extras) {
+ public void onPlayFromUri(String packageName, int pid, int uid, Uri uri, Bundle extras) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPlayFromUri(uri, extras);
+ session.dispatchPlayFromUri(uri, createExtraBundle(packageName, pid, uid, extras));
}
}
@Override
- public void onSkipToTrack(long id) {
+ public void onSkipToTrack(String packageName, int pid, int uid, long id) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchSkipToItem(id);
+ session.dispatchSkipToItem(id, createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onPause() {
+ public void onPause(String packageName, int pid, int uid) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPause();
+ session.dispatchPause(createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onStop() {
+ public void onStop(String packageName, int pid, int uid) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchStop();
+ session.dispatchStop(createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onNext() {
+ public void onNext(String packageName, int pid, int uid) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchNext();
+ session.dispatchNext(createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onPrevious() {
+ public void onPrevious(String packageName, int pid, int uid) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchPrevious();
+ session.dispatchPrevious(createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onFastForward() {
+ public void onFastForward(String packageName, int pid, int uid) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchFastForward();
+ session.dispatchFastForward(createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onRewind() {
+ public void onRewind(String packageName, int pid, int uid) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchRewind();
+ session.dispatchRewind(createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onSeekTo(long pos) {
+ public void onSeekTo(String packageName, int pid, int uid, long pos) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchSeekTo(pos);
+ session.dispatchSeekTo(pos, createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onRate(Rating rating) {
+ public void onRate(String packageName, int pid, int uid, Rating rating) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchRate(rating);
+ session.dispatchRate(rating, createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onCustomAction(String action, Bundle args) {
+ public void onCustomAction(String packageName, int pid, int uid, String action,
+ Bundle args) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchCustomAction(action, args);
+ session.dispatchCustomAction(
+ action, createExtraBundle(packageName, pid, uid, args));
}
}
@Override
- public void onAdjustVolume(int direction) {
+ public void onAdjustVolume(String packageName, int pid, int uid, int direction) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchAdjustVolume(direction);
+ session.dispatchAdjustVolume(direction, createExtraBundle(packageName, pid, uid));
}
}
@Override
- public void onSetVolumeTo(int value) {
+ public void onSetVolumeTo(String packageName, int pid, int uid, int value) {
MediaSession session = mMediaSession.get();
if (session != null) {
- session.dispatchSetVolumeTo(value);
+ session.dispatchSetVolumeTo(value, createExtraBundle(packageName, pid, uid));
}
}
+ private Bundle createExtraBundle(String packageName, int pid, int uid) {
+ return createExtraBundle(packageName, pid, uid, null);
+ }
+
+ private Bundle createExtraBundle(String packageName, int pid, int uid,
+ Bundle originalBundle) {
+ Bundle bundle = new Bundle();
+ bundle.putString(CallbackMessageHandler.EXTRA_KEY_CALLING_PACKAGE, packageName);
+ bundle.putInt(CallbackMessageHandler.EXTRA_KEY_CALLING_PID, pid);
+ bundle.putInt(CallbackMessageHandler.EXTRA_KEY_CALLING_UID, uid);
+ if (originalBundle != null) {
+ bundle.putBundle(CallbackMessageHandler.EXTRA_KEY_ORIGINAL_BUNDLE, originalBundle);
+ }
+ return bundle;
+ }
}
/**
@@ -1271,7 +1321,8 @@ public final class MediaSession {
return 0;
}
- public static final Creator<MediaSession.QueueItem> CREATOR = new Creator<MediaSession.QueueItem>() {
+ public static final Creator<MediaSession.QueueItem> CREATOR =
+ new Creator<MediaSession.QueueItem>() {
@Override
public MediaSession.QueueItem createFromParcel(Parcel p) {
@@ -1328,6 +1379,15 @@ public final class MediaSession {
private class CallbackMessageHandler extends Handler {
+ private static final String EXTRA_KEY_CALLING_PACKAGE =
+ "android.media.session.extra.CALLING_PACKAGE";
+ private static final String EXTRA_KEY_CALLING_PID =
+ "android.media.session.extra.CALLING_PID";
+ private static final String EXTRA_KEY_CALLING_UID =
+ "android.media.session.extra.CALLING_UID";
+ private static final String EXTRA_KEY_ORIGINAL_BUNDLE =
+ "android.media.session.extra.ORIGINAL_BUNDLE";
+
private static final int MSG_COMMAND = 1;
private static final int MSG_MEDIA_BUTTON = 2;
private static final int MSG_PREPARE = 3;
@@ -1354,6 +1414,8 @@ public final class MediaSession {
private MediaSession.Callback mCallback;
+ private RemoteUserInfo mCurrentControllerInfo;
+
public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
super(looper, null, true);
mCallback = callback;
@@ -1366,21 +1428,17 @@ public final class MediaSession {
msg.sendToTarget();
}
- public void post(int what, Object obj) {
- obtainMessage(what, obj).sendToTarget();
- }
-
- public void post(int what) {
- post(what, null);
- }
-
- public void post(int what, Object obj, int arg1) {
- obtainMessage(what, arg1, 0, obj).sendToTarget();
- }
-
@Override
public void handleMessage(Message msg) {
VolumeProvider vp;
+ Bundle bundle = msg.getData();
+ Bundle originalBundle = bundle.getBundle(EXTRA_KEY_ORIGINAL_BUNDLE);
+
+ mCurrentControllerInfo = new RemoteUserInfo(
+ bundle.getString(EXTRA_KEY_CALLING_PACKAGE),
+ bundle.getInt(EXTRA_KEY_CALLING_PID, INVALID_PID),
+ bundle.getInt(EXTRA_KEY_CALLING_UID, INVALID_UID));
+
switch (msg.what) {
case MSG_COMMAND:
Command cmd = (Command) msg.obj;
@@ -1393,25 +1451,25 @@ public final class MediaSession {
mCallback.onPrepare();
break;
case MSG_PREPARE_MEDIA_ID:
- mCallback.onPrepareFromMediaId((String) msg.obj, msg.getData());
+ mCallback.onPrepareFromMediaId((String) msg.obj, originalBundle);
break;
case MSG_PREPARE_SEARCH:
- mCallback.onPrepareFromSearch((String) msg.obj, msg.getData());
+ mCallback.onPrepareFromSearch((String) msg.obj, originalBundle);
break;
case MSG_PREPARE_URI:
- mCallback.onPrepareFromUri((Uri) msg.obj, msg.getData());
+ mCallback.onPrepareFromUri((Uri) msg.obj, originalBundle);
break;
case MSG_PLAY:
mCallback.onPlay();
break;
case MSG_PLAY_MEDIA_ID:
- mCallback.onPlayFromMediaId((String) msg.obj, msg.getData());
+ mCallback.onPlayFromMediaId((String) msg.obj, originalBundle);
break;
case MSG_PLAY_SEARCH:
- mCallback.onPlayFromSearch((String) msg.obj, msg.getData());
+ mCallback.onPlayFromSearch((String) msg.obj, originalBundle);
break;
case MSG_PLAY_URI:
- mCallback.onPlayFromUri((Uri) msg.obj, msg.getData());
+ mCallback.onPlayFromUri((Uri) msg.obj, originalBundle);
break;
case MSG_SKIP_TO_ITEM:
mCallback.onSkipToQueueItem((Long) msg.obj);
@@ -1441,7 +1499,7 @@ public final class MediaSession {
mCallback.onSetRating((Rating) msg.obj);
break;
case MSG_CUSTOM_ACTION:
- mCallback.onCustomAction((String) msg.obj, msg.getData());
+ mCallback.onCustomAction((String) msg.obj, originalBundle);
break;
case MSG_ADJUST_VOLUME:
synchronized (mLock) {
@@ -1463,6 +1521,7 @@ public final class MediaSession {
mCallback.handleMediaPlayPauseKeySingleTapIfPending();
break;
}
+ mCurrentControllerInfo = null;
}
}
}
diff --git a/android/media/session/MediaSessionManager.java b/android/media/session/MediaSessionManager.java
index 81b4603e..519af1ba 100644
--- a/android/media/session/MediaSessionManager.java
+++ b/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;
@@ -24,8 +25,8 @@ import android.annotation.SystemService;
import android.content.ComponentName;
import android.content.Context;
import android.media.AudioManager;
-import android.media.IMediaSession2;
import android.media.IRemoteVolumeController;
+import android.media.ISessionTokensListener;
import android.media.MediaSession2;
import android.media.MediaSessionService2;
import android.media.SessionToken2;
@@ -36,7 +37,9 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.service.media.MediaBrowserService;
import android.service.notification.NotificationListenerService;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.view.KeyEvent;
@@ -44,6 +47,7 @@ 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}
@@ -71,6 +75,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;
@@ -336,37 +342,74 @@ public final class MediaSessionManager {
}
/**
- * Called when a {@link MediaSession2} is created.
+ * Returns whether the app is trusted.
+ * <p>
+ * An app is trusted if the app holds the android.Manifest.permission.MEDIA_CONTENT_CONTROL
+ * permission or has an enabled notification listener.
*
+ * @param userInfo The remote user info
+ */
+ public boolean isTrustedForMediaControl(RemoteUserInfo userInfo) {
+ if (userInfo.getPackageName() == null) {
+ return false;
+ }
+ try {
+ return mService.isTrusted(
+ userInfo.getPackageName(), userInfo.getPid(), userInfo.getUid());
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ }
+ return false;
+ }
+
+ /**
+ * Called when a {@link MediaSession2} is created.
+ * @hide
+ */
+ public boolean createSession2(@NonNull SessionToken2 token) {
+ if (token == null) {
+ return false;
+ }
+ try {
+ return mService.createSession2(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 SessionToken2 createSessionToken(@NonNull String callingPackage, @NonNull String id,
- @NonNull IMediaSession2 binder) {
+ public void destroySession2(@NonNull SessionToken2 token) {
+ if (token == null) {
+ return;
+ }
try {
- Bundle bundle = mService.createSessionToken(callingPackage, id, binder);
- return SessionToken2.fromBundle(bundle);
+ mService.destroySession2(token.toBundle());
} catch (RemoteException e) {
Log.wtf(TAG, "Cannot communicate with the service.", e);
}
- return null;
}
/**
+ * @hide
* 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
+ * @return list of tokens
*/
- // TODO(jaewan): Unhide
- // TODO(jaewan): Protect this with permission.
- // TODO(jaewna): Add listener for change in lists.
public List<SessionToken2> getActiveSessionTokens() {
try {
List<Bundle> bundles = mService.getSessionTokens(
- /* activeSessionOnly */ true, /* sessionServiceOnly */ false);
+ /* activeSessionOnly */ true, /* sessionServiceOnly */ false,
+ mContext.getPackageName());
return toTokenList(bundles);
} catch (RemoteException e) {
Log.wtf(TAG, "Cannot communicate with the service.", e);
@@ -375,18 +418,21 @@ public final class MediaSessionManager {
}
/**
+ * @hide
* 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
+ * @return list of tokens
*/
- // TODO(jaewan): Unhide
- // TODO(jaewna): Add listener for change in lists.
public List<SessionToken2> getSessionServiceTokens() {
try {
List<Bundle> bundles = mService.getSessionTokens(
- /* activeSessionOnly */ false, /* sessionServiceOnly */ true);
+ /* activeSessionOnly */ false, /* sessionServiceOnly */ true,
+ mContext.getPackageName());
return toTokenList(bundles);
} catch (RemoteException e) {
Log.wtf(TAG, "Cannot communicate with the service.", e);
@@ -395,21 +441,23 @@ public final class MediaSessionManager {
}
/**
+ * @hide
* 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
+ * @return list of tokens
* @see #getActiveSessionTokens
* @see #getSessionServiceTokens
- * @hide
*/
- // TODO(jaewan): Unhide
- // TODO(jaewan): Protect this with permission.
- // TODO(jaewna): Add listener for change in lists.
public List<SessionToken2> getAllSessionTokens() {
try {
List<Bundle> bundles = mService.getSessionTokens(
- /* activeSessionOnly */ false, /* sessionServiceOnly */ false);
+ /* activeSessionOnly */ false, /* sessionServiceOnly */ false,
+ mContext.getPackageName());
return toTokenList(bundles);
} catch (RemoteException e) {
Log.wtf(TAG, "Cannot communicate with the service.", e);
@@ -417,6 +465,84 @@ public final class MediaSessionManager {
}
}
+ /**
+ * @hide
+ * 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.
+ */
+ 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);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * Stop receiving session token updates on the specified listener.
+ *
+ * @param listener The listener to remove.
+ */
+ 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, mContext.getPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in removeSessionTokensListener.", e);
+ } finally {
+ wrapper.release();
+ }
+ }
+ }
+ }
+
private static List<SessionToken2> toTokenList(List<Bundle> bundles) {
List<SessionToken2> tokens = new ArrayList<>();
if (bundles != null) {
@@ -552,6 +678,15 @@ public final class MediaSessionManager {
}
/**
+ * @hide
+ * Listens for changes to the {@link #getAllSessionTokens()}. This can be added
+ * using {@link #addOnActiveSessionsChangedListener}.
+ */
+ public interface OnSessionTokensChangedListener {
+ void onSessionTokensChanged(@NonNull List<SessionToken2> tokens);
+ }
+
+ /**
* Listens the volume key long-presses.
* @hide
*/
@@ -631,6 +766,56 @@ public final class MediaSessionManager {
public abstract void onAddressedPlayerChanged(ComponentName mediaButtonReceiver);
}
+ /**
+ * Information of a remote user of {@link MediaSession} or {@link MediaBrowserService}.
+ * This can be used to decide whether the remote user is trusted app.
+ *
+ * @see #isTrustedForMediaControl(RemoteUserInfo)
+ */
+ public static final class RemoteUserInfo {
+ private String mPackageName;
+ private int mPid;
+ private int mUid;
+
+ public RemoteUserInfo(String packageName, int pid, int uid) {
+ mPackageName = packageName;
+ mPid = pid;
+ mUid = uid;
+ }
+
+ /**
+ * @return package name of the controller
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return pid of the controller
+ */
+ public int getPid() {
+ return mPid;
+ }
+
+ /**
+ * @return uid of the controller
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof RemoteUserInfo)) {
+ return false;
+ }
+ RemoteUserInfo otherUserInfo = (RemoteUserInfo) obj;
+ return TextUtils.equals(mPackageName, otherUserInfo.mPackageName)
+ && mPid == otherUserInfo.mPid
+ && mUid == otherUserInfo.mUid;
+ }
+ }
+
private static final class SessionsChangedWrapper {
private Context mContext;
private OnActiveSessionsChangedListener mListener;
@@ -653,8 +838,7 @@ public final class MediaSessionManager {
public void run() {
final Context context = mContext;
if (context != null) {
- ArrayList<MediaController> controllers
- = new ArrayList<MediaController>();
+ ArrayList<MediaController> controllers = new ArrayList<>();
int size = tokens.size();
for (int i = 0; i < size; i++) {
controllers.add(new MediaController(context, tokens.get(i)));
@@ -677,6 +861,41 @@ 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) {
+ final Executor executor = mExecutor;
+ if (executor != null) {
+ executor.execute(() -> {
+ final Context context = mContext;
+ final OnSessionTokensChangedListener listener = mListener;
+ if (context != null && listener != null) {
+ listener.onSessionTokensChanged(toTokenList(bundles));
+ }
+ });
+ }
+ }
+ };
+
+ private void release() {
+ mListener = null;
+ mContext = null;
+ mExecutor = null;
+ }
+ }
+
private static final class OnVolumeKeyLongPressListenerImpl
extends IOnVolumeKeyLongPressListener.Stub {
private OnVolumeKeyLongPressListener mListener;
diff --git a/android/media/soundtrigger/SoundTriggerDetectionService.java b/android/media/soundtrigger/SoundTriggerDetectionService.java
new file mode 100644
index 00000000..7381d977
--- /dev/null
+++ b/android/media/soundtrigger/SoundTriggerDetectionService.java
@@ -0,0 +1,292 @@
+/*
+ * 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.soundtrigger;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.soundtrigger.SoundTrigger;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.UUID;
+
+/**
+ * A service that allows interaction with the actual sound trigger detection on the system.
+ *
+ * <p> Sound trigger detection refers to detectors that match generic sound patterns that are
+ * not voice-based. The voice-based recognition models should utilize the {@link
+ * android.service.voice.VoiceInteractionService} instead. Access to this class needs to be
+ * protected by the {@value android.Manifest.permission.BIND_SOUND_TRIGGER_DETECTION_SERVICE}
+ * permission granted only to the system.
+ *
+ * <p>This service has to be explicitly started by an app, the system does not scan for and start
+ * these services.
+ *
+ * <p>If an operation ({@link #onGenericRecognitionEvent}, {@link #onError},
+ * {@link #onRecognitionPaused}, {@link #onRecognitionResumed}) is triggered the service is
+ * considered as running in the foreground. Once the operation is processed the service should call
+ * {@link #operationFinished(UUID, int)}. If this does not happen in
+ * {@link SoundTriggerManager#getDetectionServiceOperationsTimeout()} milliseconds
+ * {@link #onStopOperation(UUID, Bundle, int)} is called and the service is unbound.
+ *
+ * <p>The total amount of operations per day might be limited.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class SoundTriggerDetectionService extends Service {
+ private static final String LOG_TAG = SoundTriggerDetectionService.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
+ private final Object mLock = new Object();
+
+ /**
+ * Client indexed by model uuid. This is needed for the {@link #operationFinished(UUID, int)}
+ * callbacks.
+ */
+ @GuardedBy("mLock")
+ private final ArrayMap<UUID, ISoundTriggerDetectionServiceClient> mClients =
+ new ArrayMap<>();
+
+ private Handler mHandler;
+
+ /**
+ * @hide
+ */
+ @Override
+ protected final void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ mHandler = new Handler(base.getMainLooper());
+ }
+
+ private void setClient(@NonNull UUID uuid, @Nullable Bundle params,
+ @NonNull ISoundTriggerDetectionServiceClient client) {
+ if (DEBUG) Log.i(LOG_TAG, uuid + ": handle setClient");
+
+ synchronized (mLock) {
+ mClients.put(uuid, client);
+ }
+ onConnected(uuid, params);
+ }
+
+ private void removeClient(@NonNull UUID uuid, @Nullable Bundle params) {
+ if (DEBUG) Log.i(LOG_TAG, uuid + ": handle removeClient");
+
+ synchronized (mLock) {
+ mClients.remove(uuid);
+ }
+ onDisconnected(uuid, params);
+ }
+
+ /**
+ * The system has connected to this service for the recognition registered for the model
+ * {@code uuid}.
+ *
+ * <p> This is called before any operations are delivered.
+ *
+ * @param uuid The {@code uuid} of the model the recognitions is registered for
+ * @param params The {@code params} passed when the recognition was started
+ */
+ @MainThread
+ public void onConnected(@NonNull UUID uuid, @Nullable Bundle params) {
+ /* do nothing */
+ }
+
+ /**
+ * The system has disconnected from this service for the recognition registered for the model
+ * {@code uuid}.
+ *
+ * <p>Once this is called {@link #operationFinished} cannot be called anymore for
+ * {@code uuid}.
+ *
+ * <p> {@link #onConnected(UUID, Bundle)} is called before any further operations are delivered.
+ *
+ * @param uuid The {@code uuid} of the model the recognitions is registered for
+ * @param params The {@code params} passed when the recognition was started
+ */
+ @MainThread
+ public void onDisconnected(@NonNull UUID uuid, @Nullable Bundle params) {
+ /* do nothing */
+ }
+
+ /**
+ * A new generic sound trigger event has been detected.
+ *
+ * @param uuid The {@code uuid} of the model the recognition is registered for
+ * @param params The {@code params} passed when the recognition was started
+ * @param opId The id of this operation. Once the operation is done, this service needs to call
+ * {@link #operationFinished(UUID, int)}
+ * @param event The event that has been detected
+ */
+ @MainThread
+ public void onGenericRecognitionEvent(@NonNull UUID uuid, @Nullable Bundle params, int opId,
+ @NonNull SoundTrigger.RecognitionEvent event) {
+ operationFinished(uuid, opId);
+ }
+
+ /**
+ * A error has been detected.
+ *
+ * @param uuid The {@code uuid} of the model the recognition is registered for
+ * @param params The {@code params} passed when the recognition was started
+ * @param opId The id of this operation. Once the operation is done, this service needs to call
+ * {@link #operationFinished(UUID, int)}
+ * @param status The error code detected
+ */
+ @MainThread
+ public void onError(@NonNull UUID uuid, @Nullable Bundle params, int opId, int status) {
+ operationFinished(uuid, opId);
+ }
+
+ /**
+ * An operation took too long and should be stopped.
+ *
+ * @param uuid The {@code uuid} of the model the recognition is registered for
+ * @param params The {@code params} passed when the recognition was started
+ * @param opId The id of the operation that took too long
+ */
+ @MainThread
+ public abstract void onStopOperation(@NonNull UUID uuid, @Nullable Bundle params, int opId);
+
+ /**
+ * Tell that the system that an operation has been fully processed.
+ *
+ * @param uuid The {@code uuid} of the model the recognition is registered for
+ * @param opId The id of the operation that is processed
+ */
+ public final void operationFinished(@Nullable UUID uuid, int opId) {
+ try {
+ ISoundTriggerDetectionServiceClient client;
+ synchronized (mLock) {
+ client = mClients.get(uuid);
+
+ if (client == null) {
+ throw new IllegalStateException("operationFinished called, but no client for "
+ + uuid + ". Was this called after onDisconnected?");
+ }
+ }
+ client.onOpFinished(opId);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ return new ISoundTriggerDetectionService.Stub() {
+ private final Object mBinderLock = new Object();
+
+ /** Cached params bundles indexed by the model uuid */
+ @GuardedBy("mBinderLock")
+ public final ArrayMap<UUID, Bundle> mParams = new ArrayMap<>();
+
+ @Override
+ public void setClient(ParcelUuid puuid, Bundle params,
+ ISoundTriggerDetectionServiceClient client) {
+ UUID uuid = puuid.getUuid();
+ synchronized (mBinderLock) {
+ mParams.put(uuid, params);
+ }
+
+ if (DEBUG) Log.i(LOG_TAG, uuid + ": setClient(" + params + ")");
+ mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::setClient,
+ SoundTriggerDetectionService.this, uuid, params, client));
+ }
+
+ @Override
+ public void removeClient(ParcelUuid puuid) {
+ UUID uuid = puuid.getUuid();
+ Bundle params;
+ synchronized (mBinderLock) {
+ params = mParams.remove(uuid);
+ }
+
+ if (DEBUG) Log.i(LOG_TAG, uuid + ": removeClient");
+ mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::removeClient,
+ SoundTriggerDetectionService.this, uuid, params));
+ }
+
+ @Override
+ public void onGenericRecognitionEvent(ParcelUuid puuid, int opId,
+ SoundTrigger.GenericRecognitionEvent event) {
+ UUID uuid = puuid.getUuid();
+ Bundle params;
+ synchronized (mBinderLock) {
+ params = mParams.get(uuid);
+ }
+
+ if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onGenericRecognitionEvent");
+ mHandler.sendMessage(
+ obtainMessage(SoundTriggerDetectionService::onGenericRecognitionEvent,
+ SoundTriggerDetectionService.this, uuid, params, opId, event));
+ }
+
+ @Override
+ public void onError(ParcelUuid puuid, int opId, int status) {
+ UUID uuid = puuid.getUuid();
+ Bundle params;
+ synchronized (mBinderLock) {
+ params = mParams.get(uuid);
+ }
+
+ if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onError(" + status + ")");
+ mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::onError,
+ SoundTriggerDetectionService.this, uuid, params, opId, status));
+ }
+
+ @Override
+ public void onStopOperation(ParcelUuid puuid, int opId) {
+ UUID uuid = puuid.getUuid();
+ Bundle params;
+ synchronized (mBinderLock) {
+ params = mParams.get(uuid);
+ }
+
+ if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onStopOperation");
+ mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::onStopOperation,
+ SoundTriggerDetectionService.this, uuid, params, opId));
+ }
+ };
+ }
+
+ @CallSuper
+ @Override
+ public boolean onUnbind(Intent intent) {
+ mClients.clear();
+
+ return false;
+ }
+}
diff --git a/android/media/soundtrigger/SoundTriggerManager.java b/android/media/soundtrigger/SoundTriggerManager.java
index 92ffae0f..c9ec7526 100644
--- a/android/media/soundtrigger/SoundTriggerManager.java
+++ b/android/media/soundtrigger/SoundTriggerManager.java
@@ -15,26 +15,31 @@
*/
package android.media.soundtrigger;
+
import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
-import android.app.PendingIntent;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Context;
import android.hardware.soundtrigger.SoundTrigger;
-import android.hardware.soundtrigger.SoundTrigger.SoundModel;
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.hardware.soundtrigger.SoundTrigger.SoundModel;
+import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelUuid;
import android.os.RemoteException;
+import android.provider.Settings;
import android.util.Slog;
import com.android.internal.app.ISoundTriggerService;
+import com.android.internal.util.Preconditions;
import java.util.HashMap;
import java.util.UUID;
@@ -276,6 +281,40 @@ public final class SoundTriggerManager {
}
/**
+ * Starts recognition for the given model id. All events from the model will be sent to the
+ * service.
+ *
+ * <p>This only supports generic sound trigger events. For keyphrase events, please use
+ * {@link android.service.voice.VoiceInteractionService}.
+ *
+ * @param soundModelId Id of the sound model
+ * @param params Opaque data sent to each service call of the service as the {@code params}
+ * argument
+ * @param detectionService The component name of the service that should receive the events.
+ * Needs to subclass {@link SoundTriggerDetectionService}
+ * @param config Configures the recognition
+ *
+ * @return {@link SoundTrigger#STATUS_OK} if the recognition could be started, error code
+ * otherwise
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+ public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params,
+ @NonNull ComponentName detectionService, @NonNull RecognitionConfig config) {
+ Preconditions.checkNotNull(soundModelId);
+ Preconditions.checkNotNull(detectionService);
+ Preconditions.checkNotNull(config);
+
+ try {
+ return mSoundTriggerService.startRecognitionForService(new ParcelUuid(soundModelId),
+ params, detectionService, config);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Stops the given model's recognition.
* @hide
*/
@@ -324,4 +363,19 @@ public final class SoundTriggerManager {
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Get the amount of time (in milliseconds) an operation of the
+ * {@link ISoundTriggerDetectionService} is allowed to ask.
+ *
+ * @return The amount of time an sound trigger detection service operation is allowed to last
+ */
+ public int getDetectionServiceOperationsTimeout() {
+ try {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT);
+ } catch (Settings.SettingNotFoundException e) {
+ return Integer.MAX_VALUE;
+ }
+ }
}
diff --git a/android/media/update/ApiLoader.java b/android/media/update/ApiLoader.java
index b928e931..6f82f683 100644
--- a/android/media/update/ApiLoader.java
+++ b/android/media/update/ApiLoader.java
@@ -16,45 +16,68 @@
package android.media.update;
-import android.content.res.Resources;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.internal.annotations.GuardedBy;
+
+import dalvik.system.PathClassLoader;
+
+import java.io.File;
/**
* @hide
*/
public final class ApiLoader {
- private static Object sMediaLibrary;
+ @GuardedBy("this")
+ private static StaticProvider sMediaUpdatable;
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 static final boolean REGISTER_UPDATE_DEPENDENCY = true;
private ApiLoader() { }
- public static StaticProvider getProvider(Context context) {
+ public static StaticProvider getProvider() {
+ if (sMediaUpdatable != null) return sMediaUpdatable;
+
try {
- return (StaticProvider) getMediaLibraryImpl(context);
- } catch (PackageManager.NameNotFoundException | ReflectiveOperationException e) {
+ return getMediaUpdatable();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (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;
+ private static synchronized StaticProvider getMediaUpdatable()
+ throws NameNotFoundException, ReflectiveOperationException, RemoteException {
+ if (sMediaUpdatable != null) return sMediaUpdatable;
// 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;
+ flags |= PackageManager.GET_SHARED_LIBRARY_FILES;
+ ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo(
+ UPDATE_PACKAGE, flags, UserHandle.myUserId());
+
+ if (REGISTER_UPDATE_DEPENDENCY) {
+ // Register a dependency to the updatable in order to be killed during updates
+ ActivityManager.getService().addPackageDependency(ai.packageName);
+ }
+
+ ClassLoader classLoader = new PathClassLoader(ai.sourceDir,
+ ai.nativeLibraryDir + File.pathSeparator + System.getProperty("java.library.path"),
+ ClassLoader.getSystemClassLoader().getParent());
+ return sMediaUpdatable = (StaticProvider) classLoader.loadClass(UPDATE_CLASS)
+ .getMethod(UPDATE_METHOD, ApplicationInfo.class).invoke(null, ai);
}
}
diff --git a/android/media/update/MediaBrowser2Provider.java b/android/media/update/MediaBrowser2Provider.java
index e48711d9..a18701ec 100644
--- a/android/media/update/MediaBrowser2Provider.java
+++ b/android/media/update/MediaBrowser2Provider.java
@@ -22,12 +22,13 @@ import android.os.Bundle;
* @hide
*/
public interface MediaBrowser2Provider extends MediaController2Provider {
- void getBrowserRoot_impl(Bundle rootHints);
+ void getLibraryRoot_impl(Bundle rootHints);
- void subscribe_impl(String parentId, Bundle options);
- void unsubscribe_impl(String parentId, Bundle options);
+ void subscribe_impl(String parentId, Bundle extras);
+ void unsubscribe_impl(String parentId);
void getItem_impl(String mediaId);
- void getChildren_impl(String parentId, int page, int pageSize, Bundle options);
- void search_impl(String query, int page, int pageSize, Bundle extras);
+ void getChildren_impl(String parentId, int page, int pageSize, Bundle extras);
+ void search_impl(String query, Bundle extras);
+ void getSearchResult_impl(String query, int page, int pageSize, Bundle extras);
}
diff --git a/android/media/update/MediaControlView2Provider.java b/android/media/update/MediaControlView2Provider.java
index 6b38c926..8e69653c 100644
--- a/android/media/update/MediaControlView2Provider.java
+++ b/android/media/update/MediaControlView2Provider.java
@@ -16,9 +16,10 @@
package android.media.update;
-import android.annotation.SystemApi;
+import android.media.SessionToken2;
import android.media.session.MediaController;
-import android.view.View;
+import android.util.AttributeSet;
+import android.widget.MediaControlView2;
/**
* Interface for connecting the public API to an updatable implementation.
@@ -33,15 +34,19 @@ import android.view.View;
*
* @hide
*/
-// TODO @SystemApi
-public interface MediaControlView2Provider extends ViewProvider {
+// TODO: @SystemApi
+public interface MediaControlView2Provider extends ViewGroupProvider {
+ void initialize(AttributeSet attrs, int defStyleAttr, int defStyleRes);
+
+ void setMediaSessionToken_impl(SessionToken2 token);
+ void setOnFullScreenListener_impl(MediaControlView2.OnFullScreenListener l);
+ /**
+ * @hide TODO: remove
+ */
void setController_impl(MediaController controller);
- void show_impl();
- void show_impl(int timeout);
- boolean isShowing_impl();
- void hide_impl();
- void showSubtitle_impl();
- void hideSubtitle_impl();
- void setPrevNextListeners_impl(View.OnClickListener next, View.OnClickListener prev);
- void setButtonVisibility_impl(int button, boolean visible);
+ /**
+ * @hide
+ */
+ void setButtonVisibility_impl(int button, int visibility);
+ void requestPlayButtonFocus_impl();
}
diff --git a/android/media/update/MediaController2Provider.java b/android/media/update/MediaController2Provider.java
index c5f6b963..7234f7b6 100644
--- a/android/media/update/MediaController2Provider.java
+++ b/android/media/update/MediaController2Provider.java
@@ -17,11 +17,11 @@
package android.media.update;
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.PlaylistParam;
-import android.media.PlaybackState2;
+import android.media.MediaMetadata2;
+import android.media.SessionCommand2;
import android.media.Rating2;
import android.media.SessionToken2;
import android.net.Uri;
@@ -34,12 +34,13 @@ 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);
@@ -47,18 +48,35 @@ public interface MediaController2Provider extends TransportControlProvider {
void prepareFromUri_impl(Uri uri, Bundle extras);
void prepareFromSearch_impl(String query, Bundle extras);
- void prepareMediaId_impl(String mediaId, Bundle extras);
+ void prepareFromMediaId_impl(String mediaId, Bundle extras);
void playFromSearch_impl(String query, Bundle extras);
- void playFromUri_impl(String uri, Bundle extras);
+ void playFromUri_impl(Uri uri, Bundle extras);
void playFromMediaId_impl(String mediaId, Bundle extras);
+ void fastForward_impl();
+ void rewind_impl();
- void setRating_impl(Rating2 rating);
- void sendCustomCommand_impl(Command command, Bundle args, ResultReceiver cb);
+ void setRating_impl(String mediaId, Rating2 rating);
+ void sendCustomCommand_impl(SessionCommand2 command, Bundle args, ResultReceiver cb);
List<MediaItem2> getPlaylist_impl();
+ void setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata);
+ MediaMetadata2 getPlaylistMetadata_impl();
+ void updatePlaylistMetadata_impl(MediaMetadata2 metadata);
- void removePlaylistItem_impl(MediaItem2 index);
void addPlaylistItem_impl(int index, MediaItem2 item);
+ void replacePlaylistItem_impl(int index, MediaItem2 item);
+ void removePlaylistItem_impl(MediaItem2 item);
+
+ int getPlayerState_impl();
+ long getCurrentPosition_impl();
+ float getPlaybackSpeed_impl();
+ long getBufferedPosition_impl();
+ MediaItem2 getCurrentMediaItem_impl();
- PlaylistParam getPlaylistParam_impl();
- PlaybackState2 getPlaybackState_impl();
+ interface PlaybackInfoProvider {
+ int getPlaybackType_impl();
+ AudioAttributes getAudioAttributes_impl();
+ int getControlType_impl();
+ int getMaxVolume_impl();
+ int getCurrentVolume_impl();
+ }
}
diff --git a/android/media/update/MediaItem2Provider.java b/android/media/update/MediaItem2Provider.java
new file mode 100644
index 00000000..47db22f2
--- /dev/null
+++ b/android/media/update/MediaItem2Provider.java
@@ -0,0 +1,46 @@
+/*
+ * 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.MediaItem2;
+import android.media.MediaItem2.Builder;
+import android.media.MediaMetadata2;
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+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();
+ boolean equals_impl(Object obj);
+
+ interface BuilderProvider {
+ Builder setMediaId_impl(String mediaId);
+ Builder setMetadata_impl(MediaMetadata2 metadata);
+ Builder setDataSourceDesc_impl(DataSourceDesc dataSourceDesc);
+ MediaItem2 build_impl();
+ }
+}
diff --git a/android/media/update/MediaLibraryService2Provider.java b/android/media/update/MediaLibraryService2Provider.java
index dac57841..9a0d693a 100644
--- a/android/media/update/MediaLibraryService2Provider.java
+++ b/android/media/update/MediaLibraryService2Provider.java
@@ -17,14 +17,24 @@
package android.media.update;
import android.media.MediaSession2.ControllerInfo;
-import android.os.Bundle; /**
+import android.os.Bundle;
+
+/**
* @hide
*/
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);
+ void notifyChildrenChanged_impl(ControllerInfo controller, String parentId,
+ int itemCount, Bundle extras);
+ void notifyChildrenChanged_impl(String parentId, int itemCount, Bundle extras);
+ void notifySearchResultChanged_impl(ControllerInfo controller, String query, int itemCount,
+ Bundle extras);
+ }
+
+ interface LibraryRootProvider {
+ String getRootId_impl();
+ Bundle getExtras_impl();
}
}
diff --git a/android/media/update/MediaMetadata2Provider.java b/android/media/update/MediaMetadata2Provider.java
new file mode 100644
index 00000000..22463e92
--- /dev/null
+++ b/android/media/update/MediaMetadata2Provider.java
@@ -0,0 +1,38 @@
+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
+ */
+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);
+ float getFloat_impl(String key);
+ Bundle getExtras_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 putFloat_impl(String key, float value);
+ Builder setExtras_impl(Bundle bundle);
+ MediaMetadata2 build_impl();
+ }
+}
diff --git a/android/media/update/MediaPlaylistAgentProvider.java b/android/media/update/MediaPlaylistAgentProvider.java
new file mode 100644
index 00000000..e1522cf5
--- /dev/null
+++ b/android/media/update/MediaPlaylistAgentProvider.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.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlaylistAgent.PlaylistEventCallback;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public interface MediaPlaylistAgentProvider {
+ // final methods of MediaPlaylistAgent
+ void registerPlaylistEventCallback_impl(Executor executor, PlaylistEventCallback callback);
+ void unregisterPlaylistEventCallback_impl(PlaylistEventCallback callback);
+ void notifyPlaylistChanged_impl();
+ void notifyPlaylistMetadataChanged_impl();
+ void notifyShuffleModeChanged_impl();
+ void notifyRepeatModeChanged_impl();
+
+ // public methods of MediaPlaylistAgent
+ List<MediaItem2> getPlaylist_impl();
+ void setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata);
+ MediaMetadata2 getPlaylistMetadata_impl();
+ void updatePlaylistMetadata_impl(MediaMetadata2 metadata);
+ void addPlaylistItem_impl(int index, MediaItem2 item);
+ void removePlaylistItem_impl(MediaItem2 item);
+ void replacePlaylistItem_impl(int index, MediaItem2 item);
+ void skipToPlaylistItem_impl(MediaItem2 item);
+ void skipToPreviousItem_impl();
+ void skipToNextItem_impl();
+ int getRepeatMode_impl();
+ void setRepeatMode_impl(int repeatMode);
+ int getShuffleMode_impl();
+ void setShuffleMode_impl(int shuffleMode);
+ MediaItem2 getMediaItem_impl(DataSourceDesc dsd);
+}
diff --git a/android/media/update/MediaSession2Provider.java b/android/media/update/MediaSession2Provider.java
index 2a68ad1d..47513486 100644
--- a/android/media/update/MediaSession2Provider.java
+++ b/android/media/update/MediaSession2Provider.java
@@ -16,50 +16,117 @@
package android.media.update;
-import android.media.AudioAttributes;
+import android.app.PendingIntent;
+import android.media.AudioFocusRequest;
import android.media.MediaItem2;
+import android.media.MediaMetadata2;
import android.media.MediaPlayerBase;
+import android.media.MediaPlaylistAgent;
import android.media.MediaSession2;
-import android.media.MediaSession2.Command;
+import android.media.SessionCommand2;
import android.media.MediaSession2.CommandButton;
-import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.CommandButton.Builder;
+import android.media.SessionCommandGroup2;
import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.OnDataSourceMissingHelper;
+import android.media.MediaSession2.SessionCallback;
import android.media.SessionToken2;
-import android.media.VolumeProvider;
+import android.media.VolumeProvider2;
import android.os.Bundle;
import android.os.ResultReceiver;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
/**
* @hide
*/
public interface MediaSession2Provider extends TransportControlProvider {
void close_impl();
- void setPlayer_impl(MediaPlayerBase player);
- void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider);
+ void updatePlayer_impl(MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+ VolumeProvider2 volumeProvider);
MediaPlayerBase getPlayer_impl();
+ MediaMetadata2 getPlaylistMetadata_impl();
+ void updatePlaylistMetadata_impl(MediaMetadata2 metadata);
+ MediaPlaylistAgent getPlaylistAgent_impl();
+ VolumeProvider2 getVolumeProvider_impl();
SessionToken2 getToken_impl();
List<ControllerInfo> getConnectedControllers_impl();
void setCustomLayout_impl(ControllerInfo controller, List<CommandButton> layout);
- void setAudioAttributes_impl(AudioAttributes attributes);
- void setAudioFocusRequest_impl(int focusGain);
-
- void setAllowedCommands_impl(ControllerInfo controller, CommandGroup commands);
- void notifyMetadataChanged_impl();
- void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args,
+ void setAudioFocusRequest_impl(AudioFocusRequest afr);
+ void setAllowedCommands_impl(ControllerInfo controller, SessionCommandGroup2 commands);
+ void sendCustomCommand_impl(ControllerInfo controller, SessionCommand2 command, Bundle args,
ResultReceiver receiver);
- void sendCustomCommand_impl(Command command, Bundle args);
- void setPlaylist_impl(List<MediaItem2> playlist, MediaSession2.PlaylistParam param);
+ void sendCustomCommand_impl(SessionCommand2 command, Bundle args);
+ void addPlaylistItem_impl(int index, MediaItem2 item);
+ void removePlaylistItem_impl(MediaItem2 item);
+ void replacePlaylistItem_impl(int index, MediaItem2 item);
+ List<MediaItem2> getPlaylist_impl();
+ void setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata);
+ MediaItem2 getCurrentPlaylistItem_impl();
+ void notifyError_impl(int errorCode, Bundle extras);
+ int getPlayerState_impl();
+ long getCurrentPosition_impl();
+ long getBufferedPosition_impl();
+ void setOnDataSourceMissingHelper_impl(OnDataSourceMissingHelper helper);
+ void clearOnDataSourceMissingHelper_impl();
+
+ // TODO(jaewan): Rename and move provider
+ interface CommandProvider {
+ int getCommandCode_impl();
+ String getCustomCommand_impl();
+ Bundle getExtras_impl();
+ Bundle toBundle_impl();
+
+ boolean equals_impl(Object ob);
+ int hashCode_impl();
+ }
+
+ // TODO(jaewan): Rename and move provider
+ interface CommandGroupProvider {
+ void addCommand_impl(SessionCommand2 command);
+ void addAllPredefinedCommands_impl();
+ void removeCommand_impl(SessionCommand2 command);
+ boolean hasCommand_impl(SessionCommand2 command);
+ boolean hasCommand_impl(int code);
+ Set<SessionCommand2> getCommands_impl();
+ Bundle toBundle_impl();
+ }
+
+ interface CommandButtonProvider {
+ SessionCommand2 getCommand_impl();
+ int getIconResId_impl();
+ String getDisplayName_impl();
+ Bundle getExtras_impl();
+ boolean isEnabled_impl();
+
+ interface BuilderProvider {
+ Builder setCommand_impl(SessionCommand2 command);
+ Builder setIconResId_impl(int resId);
+ Builder setDisplayName_impl(String displayName);
+ Builder setEnabled_impl(boolean enabled);
+ Builder setExtras_impl(Bundle extras);
+ CommandButton build_impl();
+ }
+ }
- /**
- * @hide
- */
interface ControllerInfoProvider {
String getPackageName_impl();
int getUid_impl();
boolean isTrusted_impl();
int hashCode_impl();
- boolean equals_impl(ControllerInfoProvider obj);
+ boolean equals_impl(Object obj);
+ String toString_impl();
+ }
+
+ interface BuilderBaseProvider<T extends MediaSession2, C extends SessionCallback> {
+ void setPlayer_impl(MediaPlayerBase player);
+ void setPlaylistAgent_impl(MediaPlaylistAgent playlistAgent);
+ void setVolumeProvider_impl(VolumeProvider2 volumeProvider);
+ void setSessionActivity_impl(PendingIntent pi);
+ void setId_impl(String id);
+ void setSessionCallback_impl(Executor executor, C callback);
+ T build_impl();
}
}
diff --git a/android/media/update/MediaSessionService2Provider.java b/android/media/update/MediaSessionService2Provider.java
index a6b462b8..5eb62546 100644
--- a/android/media/update/MediaSessionService2Provider.java
+++ b/android/media/update/MediaSessionService2Provider.java
@@ -16,10 +16,10 @@
package android.media.update;
+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;
/**
@@ -27,9 +27,14 @@ import android.os.IBinder;
*/
public interface MediaSessionService2Provider {
MediaSession2 getSession_impl();
- MediaNotification onUpdateNotification_impl(PlaybackState2 state);
+ MediaNotification onUpdateNotification_impl();
// Service
void onCreate_impl();
IBinder onBind_impl(Intent intent);
+
+ interface MediaNotificationProvider {
+ int getNotificationId_impl();
+ Notification getNotification_impl();
+ }
}
diff --git a/android/media/update/ProviderCreator.java b/android/media/update/ProviderCreator.java
new file mode 100644
index 00000000..f5f3e470
--- /dev/null
+++ b/android/media/update/ProviderCreator.java
@@ -0,0 +1,23 @@
+/*
+ * 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 */
+@FunctionalInterface
+public interface ProviderCreator<T, U> {
+ U createProvider(T instance);
+}
diff --git a/android/media/update/Rating2Provider.java b/android/media/update/Rating2Provider.java
new file mode 100644
index 00000000..28ad2735
--- /dev/null
+++ b/android/media/update/Rating2Provider.java
@@ -0,0 +1,36 @@
+/*
+ * 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
+ */
+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();
+}
diff --git a/android/media/update/SessionToken2Provider.java b/android/media/update/SessionToken2Provider.java
new file mode 100644
index 00000000..95d6ce07
--- /dev/null
+++ b/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/android/media/update/StaticProvider.java b/android/media/update/StaticProvider.java
index 7c222c3c..8687b802 100644
--- a/android/media/update/StaticProvider.java
+++ b/android/media/update/StaticProvider.java
@@ -17,24 +17,37 @@
package android.media.update;
import android.annotation.Nullable;
-import android.app.PendingIntent;
+import android.app.Notification;
import android.content.Context;
-import android.media.IMediaSession2Callback;
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.MediaLibrarySessionCallback;
-import android.media.MediaPlayerBase;
+import android.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import android.media.MediaMetadata2;
+import android.media.MediaPlaylistAgent;
import android.media.MediaSession2;
import android.media.MediaSession2.SessionCallback;
import android.media.MediaSessionService2;
+import android.media.MediaSessionService2.MediaNotification;
+import android.media.Rating2;
+import android.media.SessionCommand2;
+import android.media.SessionCommandGroup2;
import android.media.SessionToken2;
-import android.media.VolumeProvider;
-import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
+import android.media.VolumeProvider2;
+import android.media.update.MediaLibraryService2Provider.LibraryRootProvider;
+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.MediaSessionService2Provider.MediaNotificationProvider;
+import android.os.Bundle;
+import android.os.IInterface;
import android.util.AttributeSet;
import android.widget.MediaControlView2;
import android.widget.VideoView2;
@@ -46,36 +59,71 @@ import java.util.concurrent.Executor;
*
* This interface provides access to constructors and static methods that are otherwise not directly
* accessible via an implementation object.
- *
* @hide
*/
-// TODO @SystemApi
public interface StaticProvider {
- MediaControlView2Provider createMediaControlView2(
- MediaControlView2 instance, ViewProvider superProvider);
- VideoView2Provider createVideoView2(
- VideoView2 instance, ViewProvider superProvider,
+ 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);
- MediaSession2Provider createMediaSession2(MediaSession2 mediaSession2, Context context,
- MediaPlayerBase player, String id, Executor callbackExecutor, SessionCallback callback,
- VolumeProvider volumeProvider, int ratingType,
- PendingIntent sessionActivity);
- ControllerInfoProvider createMediaSession2ControllerInfoProvider(
- MediaSession2.ControllerInfo instance, Context context, int uid, int pid,
- String packageName, IMediaSession2Callback callback);
- MediaController2Provider createMediaController2(
- MediaController2 instance, Context context, SessionToken2 token,
- ControllerCallback callback, Executor executor);
- MediaBrowser2Provider createMediaBrowser2(
- MediaBrowser2 instance, Context context, SessionToken2 token,
- BrowserCallback callback, Executor executor);
- MediaSessionService2Provider createMediaSessionService2(
- MediaSessionService2 instance);
- MediaSessionService2Provider createMediaLibraryService2(
- MediaLibraryService2 instance);
- MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(
- MediaLibrarySession instance, Context context, MediaPlayerBase player, String id,
- Executor callbackExecutor, MediaLibrarySessionCallback callback,
- VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity);
+ CommandProvider createMediaSession2Command(SessionCommand2 instance,
+ int commandCode, String action, Bundle extra);
+ SessionCommand2 fromBundle_MediaSession2Command(Bundle bundle);
+ CommandGroupProvider createMediaSession2CommandGroup(SessionCommandGroup2 instance,
+ SessionCommandGroup2 others);
+ SessionCommandGroup2 fromBundle_MediaSession2CommandGroup(Bundle bundle);
+ ControllerInfoProvider createMediaSession2ControllerInfo(Context context,
+ MediaSession2.ControllerInfo instance, int uid, int pid,
+ String packageName, IInterface callback);
+ CommandButtonProvider.BuilderProvider createMediaSession2CommandButtonBuilder(
+ MediaSession2.CommandButton.Builder instance);
+ BuilderBaseProvider<MediaSession2, SessionCallback> createMediaSession2Builder(
+ Context context, MediaSession2.Builder instance);
+
+ 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(
+ MediaNotification mediaNotification, int notificationId, Notification notification);
+
+ MediaSessionService2Provider createMediaLibraryService2(MediaLibraryService2 instance);
+ BuilderBaseProvider<MediaLibrarySession, MediaLibrarySessionCallback>
+ createMediaLibraryService2Builder(
+ MediaLibraryService2 service, MediaLibrarySession.Builder instance,
+ Executor callbackExecutor, MediaLibrarySessionCallback callback);
+ LibraryRootProvider createMediaLibraryService2LibraryRoot(LibraryRoot instance, String rootId,
+ Bundle extras);
+
+ SessionToken2Provider createSessionToken2(Context context, SessionToken2 instance,
+ String packageName, String serviceName, int uid);
+ SessionToken2 fromBundle_SessionToken2(Bundle bundle);
+
+ MediaItem2Provider.BuilderProvider createMediaItem2Builder(MediaItem2.Builder instance,
+ int flags);
+ MediaItem2 fromBundle_MediaItem2(Bundle bundle);
+
+ VolumeProvider2Provider createVolumeProvider2(VolumeProvider2 instance, int controlType,
+ int maxVolume, int currentVolume);
+
+ MediaMetadata2 fromBundle_MediaMetadata2(Bundle bundle);
+ MediaMetadata2Provider.BuilderProvider createMediaMetadata2Builder(
+ MediaMetadata2.Builder instance);
+ MediaMetadata2Provider.BuilderProvider createMediaMetadata2Builder(
+ MediaMetadata2.Builder instance, MediaMetadata2 source);
+
+ Rating2 newUnratedRating_Rating2(int ratingStyle);
+ Rating2 fromBundle_Rating2(Bundle bundle);
+ Rating2 newHeartRating_Rating2(boolean hasHeart);
+ Rating2 newThumbRating_Rating2(boolean thumbIsUp);
+ Rating2 newStarRating_Rating2(int starRatingStyle, float starRating);
+ Rating2 newPercentageRating_Rating2(float percent);
+
+ MediaPlaylistAgentProvider createMediaPlaylistAgent(MediaPlaylistAgent instance);
}
diff --git a/android/media/update/TransportControlProvider.java b/android/media/update/TransportControlProvider.java
index 5217a9d9..d89a88ab 100644
--- a/android/media/update/TransportControlProvider.java
+++ b/android/media/update/TransportControlProvider.java
@@ -16,24 +16,24 @@
package android.media.update;
-import android.media.MediaPlayerBase;
-import android.media.session.PlaybackState;
-import android.os.Handler;
+import android.media.MediaItem2;
/**
* @hide
*/
-// TODO(jaewan): SystemApi
public interface TransportControlProvider {
void play_impl();
void pause_impl();
void stop_impl();
- void skipToPrevious_impl();
- void skipToNext_impl();
+ void skipToPreviousItem_impl();
+ void skipToNextItem_impl();
void prepare_impl();
- void fastForward_impl();
- void rewind_impl();
void seekTo_impl(long pos);
- void setCurrentPlaylistItem_impl(int index);
+ void skipToPlaylistItem_impl(MediaItem2 item);
+
+ int getRepeatMode_impl();
+ void setRepeatMode_impl(int repeatMode);
+ int getShuffleMode_impl();
+ void setShuffleMode_impl(int shuffleMode);
}
diff --git a/android/media/update/VideoView2Provider.java b/android/media/update/VideoView2Provider.java
index 416ea98d..27b436fd 100644
--- a/android/media/update/VideoView2Provider.java
+++ b/android/media/update/VideoView2Provider.java
@@ -16,14 +16,26 @@
package android.media.update;
+import android.annotation.SystemApi;
import android.media.AudioAttributes;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
import android.media.MediaPlayerBase;
+import android.media.SessionToken2;
+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 com.android.internal.annotations.VisibleForTesting;
+
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Executor;
/**
* Interface for connecting the public API to an updatable implementation.
@@ -39,35 +51,49 @@ import java.util.Map;
* @hide
*/
// TODO @SystemApi
-public interface VideoView2Provider extends ViewProvider {
- void setMediaControlView2_impl(MediaControlView2 mediaControlView);
+public interface VideoView2Provider extends ViewGroupProvider {
+ void initialize(AttributeSet attrs, int defStyleAttr, int defStyleRes);
+
+ void setMediaControlView2_impl(MediaControlView2 mediaControlView, long intervalMs);
+ void setMediaMetadata_impl(MediaMetadata2 metadata);
+ /**
+ * @hide TODO: remove
+ */
+ MediaController getMediaController_impl();
+ SessionToken2 getMediaSessionToken_impl();
MediaControlView2 getMediaControlView2_impl();
- void start_impl();
- void pause_impl();
- int getDuration_impl();
- int getCurrentPosition_impl();
- void seekTo_impl(int msec);
- boolean isPlaying_impl();
- int getBufferPercentage_impl();
- int getAudioSessionId_impl();
- void showSubtitle_impl();
- void hideSubtitle_impl();
- void setFullScreen_impl(boolean fullScreen);
+ MediaMetadata2 getMediaMetadata_impl();
+ void setSubtitleEnabled_impl(boolean enable);
+ boolean isSubtitleEnabled_impl();
+ // TODO: remove setSpeed_impl once MediaController2 is ready.
void setSpeed_impl(float speed);
- float getSpeed_impl();
void setAudioFocusRequest_impl(int focusGain);
void setAudioAttributes_impl(AudioAttributes attributes);
- void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerBase player);
void setVideoPath_impl(String path);
- void setVideoURI_impl(Uri uri);
- void setVideoURI_impl(Uri uri, Map<String, String> headers);
+ /**
+ * @hide TODO: remove
+ */
+ void setVideoUri_impl(Uri uri);
+ /**
+ * @hide TODO: remove
+ */
+ void setVideoUri_impl(Uri uri, Map<String, String> headers);
+ void setMediaItem_impl(MediaItem2 mediaItem);
+ void setDataSource_impl(DataSourceDesc dsd);
void setViewType_impl(int viewType);
int getViewType_impl();
- void stopPlayback_impl();
- void setOnPreparedListener_impl(VideoView2.OnPreparedListener l);
- void setOnCompletionListener_impl(VideoView2.OnCompletionListener l);
- void setOnErrorListener_impl(VideoView2.OnErrorListener l);
- void setOnInfoListener_impl(VideoView2.OnInfoListener l);
+ /**
+ * @hide TODO: remove
+ */
+ void setCustomActions_impl(List<PlaybackState.CustomAction> actionList,
+ Executor executor, VideoView2.OnCustomActionListener listener);
+ /**
+ * @hide
+ */
+ @VisibleForTesting
void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l);
- void setFullScreenChangedListener_impl(VideoView2.OnFullScreenChangedListener l);
+ /**
+ * @hide TODO: remove
+ */
+ void setFullScreenRequestListener_impl(VideoView2.OnFullScreenRequestListener l);
}
diff --git a/android/media/update/ViewGroupHelper.java b/android/media/update/ViewGroupHelper.java
new file mode 100644
index 00000000..6b4f15d0
--- /dev/null
+++ b/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/android/media/update/ViewProvider.java b/android/media/update/ViewGroupProvider.java
index 78c5b36f..67e8cea8 100644
--- a/android/media/update/ViewProvider.java
+++ b/android/media/update/ViewGroupProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 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.
@@ -17,9 +17,10 @@
package android.media.update;
import android.annotation.SystemApi;
-import android.graphics.Canvas;
-import android.view.KeyEvent;
+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.
@@ -35,15 +36,32 @@ import android.view.MotionEvent;
* @hide
*/
// TODO @SystemApi
-public interface ViewProvider {
- // TODO Add more (all?) methods from View
+public interface ViewGroupProvider {
+ // View methods
void onAttachedToWindow_impl();
void onDetachedFromWindow_impl();
CharSequence getAccessibilityClassName_impl();
boolean onTouchEvent_impl(MotionEvent ev);
boolean onTrackballEvent_impl(MotionEvent ev);
- boolean onKeyDown_impl(int keyCode, KeyEvent event);
void onFinishInflate_impl();
- boolean dispatchKeyEvent_impl(KeyEvent event);
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/android/media/update/VolumeProvider2Provider.java b/android/media/update/VolumeProvider2Provider.java
new file mode 100644
index 00000000..5b5cfd32
--- /dev/null
+++ b/android/media/update/VolumeProvider2Provider.java
@@ -0,0 +1,26 @@
+/*
+ * 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
+ */
+public interface VolumeProvider2Provider {
+ int getControlType_impl();
+ int getMaxVolume_impl();
+ int getCurrentVolume_impl();
+ void setCurrentVolume_impl(int currentVolume);
+}