diff options
Diffstat (limited to 'android/media')
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> </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> * <service android:name="component_name_of_your_implementation" > * <intent-filter> @@ -48,13 +48,13 @@ import java.util.concurrent.Executor; * </intent-filter> * </service></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://<multicastIP>:<port> - * - * 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 <uses-permission>} - * element. - * - * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission - * when used with network-based content. - * - * <a name="Callbacks"></a> - * <h3>Callbacks</h3> - * <p>Applications may want to register for informational and error - * events in order to be informed of some internal state update and - * possible runtime errors during playback or streaming. Registration for - * these events is done by properly setting the appropriate listeners (via calls - * to - * {@link #registerEventCallback(Executor, EventCallback)}, - * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}). - * In order to receive the respective callback - * associated with these listeners, applications are required to create - * MediaPlayer2 objects on a thread with its own Looper running (main UI - * thread by default has a Looper running). - * * @hide */ public final class MediaPlayer2Impl extends MediaPlayer2 { @@ -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://<multicastIP>:<port> - * - * 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 < 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); +} |