From 2f8382157bfd66f6b1a3c127dbb417dd1ca56b35 Mon Sep 17 00:00:00 2001 From: Gael Lassure Date: Thu, 27 Jan 2022 17:27:29 +1100 Subject: [PATCH] Add Audio Presentation APIs to TIF Add APIs to obtain list of audio presentation from an NGA service and to enable the user to select a given one. Bug: 230776612 Change-Id: Id25ef9f27fadc1dffd846698348ddf37d6a0112e (cherry picked from commit 19a92e4b0107ab1dc63244a3e8b3cfecdcc80108) --- core/api/current.txt | 14 +- .../java/android/media/AudioPresentation.aidl | 19 ++ .../java/android/media/AudioPresentation.java | 79 +++++-- .../java/android/media/tv/ITvInputClient.aidl | 3 + .../android/media/tv/ITvInputManager.aidl | 3 + .../android/media/tv/ITvInputSession.aidl | 2 + .../media/tv/ITvInputSessionCallback.aidl | 3 + .../media/tv/ITvInputSessionWrapper.java | 19 +- .../java/android/media/tv/TvInputManager.java | 193 ++++++++++++++++++ .../java/android/media/tv/TvInputService.java | 93 +++++++++ media/java/android/media/tv/TvView.java | 83 ++++++++ .../server/tv/TvInputManagerService.java | 60 ++++++ 12 files changed, 554 insertions(+), 17 deletions(-) create mode 100644 media/java/android/media/AudioPresentation.aidl mode change 100755 => 100644 media/java/android/media/tv/TvInputService.java mode change 100755 => 100644 services/core/java/com/android/server/tv/TvInputManagerService.java diff --git a/core/api/current.txt b/core/api/current.txt index 53890eb5539df..66c3284139d57 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -21369,7 +21369,8 @@ package android.media { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } - public final class AudioPresentation { + public final class AudioPresentation implements android.os.Parcelable { + method public int describeContents(); method public java.util.Map getLabels(); method public java.util.Locale getLocale(); method public int getMasteringIndication(); @@ -21378,6 +21379,7 @@ package android.media { method public boolean hasAudioDescription(); method public boolean hasDialogueEnhancement(); method public boolean hasSpokenSubtitles(); + method public void writeToParcel(@NonNull android.os.Parcel, int); field public static final int CONTENT_COMMENTARY = 5; // 0x5 field public static final int CONTENT_DIALOG = 4; // 0x4 field public static final int CONTENT_EMERGENCY = 6; // 0x6 @@ -21387,11 +21389,14 @@ package android.media { field public static final int CONTENT_UNKNOWN = -1; // 0xffffffff field public static final int CONTENT_VISUALLY_IMPAIRED = 2; // 0x2 field public static final int CONTENT_VOICEOVER = 7; // 0x7 + field @NonNull public static final android.os.Parcelable.Creator CREATOR; field public static final int MASTERED_FOR_3D = 3; // 0x3 field public static final int MASTERED_FOR_HEADPHONE = 4; // 0x4 field public static final int MASTERED_FOR_STEREO = 1; // 0x1 field public static final int MASTERED_FOR_SURROUND = 2; // 0x2 field public static final int MASTERING_NOT_INDICATED = 0; // 0x0 + field public static final int PRESENTATION_ID_UNKNOWN = -1; // 0xffffffff + field public static final int PROGRAM_ID_UNKNOWN = -1; // 0xffffffff } public static final class AudioPresentation.Builder { @@ -27062,6 +27067,8 @@ package android.media.tv { method public void layoutSurface(int, int, int, int); method public void notifyAdResponse(@NonNull android.media.tv.AdResponse); method public void notifyAitInfoUpdated(@NonNull android.media.tv.AitInfo); + method public void notifyAudioPresentationChanged(@NonNull java.util.List); + method public void notifyAudioPresentationSelected(int, int); method public void notifyBroadcastInfoResponse(@NonNull android.media.tv.BroadcastInfoResponse); method public void notifyChannelRetuned(android.net.Uri); method public void notifyContentAllowed(); @@ -27085,6 +27092,7 @@ package android.media.tv { method public void onRemoveBroadcastInfo(int); method public void onRequestAd(@NonNull android.media.tv.AdRequest); method public void onRequestBroadcastInfo(@NonNull android.media.tv.BroadcastInfoRequest); + method public boolean onSelectAudioPresentation(int, int); method public boolean onSelectTrack(int, @Nullable String); method public abstract void onSetCaptionEnabled(boolean); method public void onSetInteractiveAppNotificationEnabled(boolean); @@ -27181,10 +27189,12 @@ package android.media.tv { ctor public TvView(android.content.Context, android.util.AttributeSet); ctor public TvView(android.content.Context, android.util.AttributeSet, int); method public boolean dispatchUnhandledInputEvent(android.view.InputEvent); + method @NonNull public java.util.List getAudioPresentations(); method public String getSelectedTrack(int); method public java.util.List getTracks(int); method public boolean onUnhandledInputEvent(android.view.InputEvent); method public void reset(); + method public void selectAudioPresentation(int, int); method public void selectTrack(int, String); method public void sendAppPrivateCommand(@NonNull String, android.os.Bundle); method public void setCallback(@Nullable android.media.tv.TvView.TvInputCallback); @@ -27217,6 +27227,8 @@ package android.media.tv { public abstract static class TvView.TvInputCallback { ctor public TvView.TvInputCallback(); method public void onAitInfoUpdated(@NonNull String, @NonNull android.media.tv.AitInfo); + method public void onAudioPresentationSelected(@NonNull String, int, int); + method public void onAudioPresentationsChanged(@NonNull String, @NonNull java.util.List); method public void onChannelRetuned(String, android.net.Uri); method public void onConnectionFailed(String); method public void onContentAllowed(String); diff --git a/media/java/android/media/AudioPresentation.aidl b/media/java/android/media/AudioPresentation.aidl new file mode 100644 index 0000000000000..3fb5a5b152e25 --- /dev/null +++ b/media/java/android/media/AudioPresentation.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 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; + +parcelable AudioPresentation; diff --git a/media/java/android/media/AudioPresentation.java b/media/java/android/media/AudioPresentation.java index 05f3c5a3512b7..a413545f3fda5 100644 --- a/media/java/android/media/AudioPresentation.java +++ b/media/java/android/media/AudioPresentation.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright (C) 2022 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. @@ -19,6 +19,9 @@ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; import android.icu.util.ULocale; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -49,7 +52,7 @@ import java.util.Objects; * Applications that parse media streams and extract presentation information on their own * can create instances of AudioPresentation by using {@link AudioPresentation.Builder} class. */ -public final class AudioPresentation { +public final class AudioPresentation implements Parcelable { private final int mPresentationId; private final int mProgramId; private final ULocale mLanguage; @@ -126,7 +129,7 @@ public final class AudioPresentation { private final boolean mAudioDescriptionAvailable; private final boolean mSpokenSubtitlesAvailable; private final boolean mDialogueEnhancementAvailable; - private final Map mLabels; + private final HashMap mLabels; /** * No preferred reproduction channel layout. @@ -160,9 +163,14 @@ public final class AudioPresentation { public static final int MASTERED_FOR_HEADPHONE = 4; /** - * This ID is reserved. No items can be explicitly assigned this ID. + * Unknown audio presentation ID, this indicates audio presentation ID is not selected. */ - private static final int UNKNOWN_ID = -1; + public static final int PRESENTATION_ID_UNKNOWN = -1; + + /** + * Unknown audio program ID, this indicates audio program ID is not selected. + */ + public static final int PROGRAM_ID_UNKNOWN = -1; /** * This allows an application developer to construct an AudioPresentation object with all the @@ -191,7 +199,7 @@ public final class AudioPresentation { boolean audioDescriptionAvailable, boolean spokenSubtitlesAvailable, boolean dialogueEnhancementAvailable, - @NonNull Map labels) { + @NonNull Map labels) { mPresentationId = presentationId; mProgramId = programId; mLanguage = language; @@ -199,7 +207,18 @@ public final class AudioPresentation { mAudioDescriptionAvailable = audioDescriptionAvailable; mSpokenSubtitlesAvailable = spokenSubtitlesAvailable; mDialogueEnhancementAvailable = dialogueEnhancementAvailable; - mLabels = new HashMap(labels); + mLabels = new HashMap(labels); + } + + private AudioPresentation(@NonNull Parcel in) { + mPresentationId = in.readInt(); + mProgramId = in.readInt(); + mLanguage = in.readSerializable(ULocale.class.getClassLoader(), ULocale.class); + mMasteringIndication = in.readInt(); + mAudioDescriptionAvailable = in.readBoolean(); + mSpokenSubtitlesAvailable = in.readBoolean(); + mDialogueEnhancementAvailable = in.readBoolean(); + mLabels = in.readSerializable(HashMap.class.getClassLoader(), HashMap.class); } /** @@ -225,13 +244,13 @@ public final class AudioPresentation { */ public Map getLabels() { Map localeLabels = new HashMap(mLabels.size()); - for (Map.Entry entry : mLabels.entrySet()) { - localeLabels.put(entry.getKey().toLocale(), entry.getValue().toString()); + for (Map.Entry entry : mLabels.entrySet()) { + localeLabels.put(entry.getKey().toLocale(), entry.getValue()); } return localeLabels; } - private Map getULabels() { + private Map getULabels() { return mLabels; } @@ -335,13 +354,13 @@ public final class AudioPresentation { */ public static final class Builder { private final int mPresentationId; - private int mProgramId = UNKNOWN_ID; + private int mProgramId = PROGRAM_ID_UNKNOWN; private ULocale mLanguage = new ULocale(""); private int mMasteringIndication = MASTERING_NOT_INDICATED; private boolean mAudioDescriptionAvailable = false; private boolean mSpokenSubtitlesAvailable = false; private boolean mDialogueEnhancementAvailable = false; - private Map mLabels = new HashMap(); + private HashMap mLabels = new HashMap(); /** * Create a {@link Builder}. Any field that should be included in the @@ -402,7 +421,10 @@ public final class AudioPresentation { * @param labels Text label indexed by its locale corresponding to the language code. */ public @NonNull Builder setLabels(@NonNull Map labels) { - mLabels = new HashMap(labels); + mLabels.clear(); + for (Map.Entry entry : labels.entrySet()) { + mLabels.put(entry.getKey(), entry.getValue().toString()); + } return this; } @@ -448,4 +470,35 @@ public final class AudioPresentation { mDialogueEnhancementAvailable, mLabels); } } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(getPresentationId()); + dest.writeInt(getProgramId()); + dest.writeSerializable(getULocale()); + dest.writeInt(getMasteringIndication()); + dest.writeBoolean(hasAudioDescription()); + dest.writeBoolean(hasSpokenSubtitles()); + dest.writeBoolean(hasDialogueEnhancement()); + dest.writeSerializable(mLabels); + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public AudioPresentation createFromParcel(@NonNull Parcel in) { + return new AudioPresentation(in); + } + + @Override + public AudioPresentation[] newArray(int size) { + return new AudioPresentation[size]; + } + }; } diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl index d55d2871311f9..c4f60c30c5474 100644 --- a/media/java/android/media/tv/ITvInputClient.aidl +++ b/media/java/android/media/tv/ITvInputClient.aidl @@ -17,6 +17,7 @@ package android.media.tv; import android.content.ComponentName; +import android.media.AudioPresentation; import android.media.tv.AdBuffer; import android.media.tv.AdResponse; import android.media.tv.AitInfo; @@ -37,6 +38,8 @@ oneway interface ITvInputClient { void onSessionReleased(int seq); void onSessionEvent(in String name, in Bundle args, int seq); void onChannelRetuned(in Uri channelUri, int seq); + void onAudioPresentationsChanged(in List AudioPresentations, int seq); + void onAudioPresentationSelected(int presentationId, int programId, int seq); void onTracksChanged(in List tracks, int seq); void onTrackSelected(int type, in String trackId, int seq); void onVideoAvailable(int seq); diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl index f7c1e3c8abde8..56aa179a64104 100644 --- a/media/java/android/media/tv/ITvInputManager.aidl +++ b/media/java/android/media/tv/ITvInputManager.aidl @@ -19,6 +19,7 @@ package android.media.tv; import android.content.ComponentName; import android.content.Intent; import android.graphics.Rect; +import android.media.AudioPresentation; import android.media.PlaybackParams; import android.media.tv.AdBuffer; import android.media.tv.AdRequest; @@ -77,6 +78,8 @@ interface ITvInputManager { void tune(in IBinder sessionToken, in Uri channelUri, in Bundle params, int userId); void setCaptionEnabled(in IBinder sessionToken, boolean enabled, int userId); void selectTrack(in IBinder sessionToken, int type, in String trackId, int userId); + void selectAudioPresentation(in IBinder sessionToken, int presentationId, int programId, + int userId); void setInteractiveAppNotificationEnabled(in IBinder sessionToken, boolean enabled, int userId); diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl index 326b98d997397..e6c09a9df14c8 100644 --- a/media/java/android/media/tv/ITvInputSession.aidl +++ b/media/java/android/media/tv/ITvInputSession.aidl @@ -17,6 +17,7 @@ package android.media.tv; import android.graphics.Rect; +import android.media.AudioPresentation; import android.media.PlaybackParams; import android.media.tv.AdBuffer; import android.media.tv.AdRequest; @@ -41,6 +42,7 @@ oneway interface ITvInputSession { void setVolume(float volume); void tune(in Uri channelUri, in Bundle params); void setCaptionEnabled(boolean enabled); + void selectAudioPresentation(int presentationId, int programId); void selectTrack(int type, in String trackId); void setInteractiveAppNotificationEnabled(boolean enable); diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl index 82166223608f6..0111f091aaa11 100644 --- a/media/java/android/media/tv/ITvInputSessionCallback.aidl +++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl @@ -16,6 +16,7 @@ package android.media.tv; +import android.media.AudioPresentation; import android.media.tv.AdBuffer; import android.media.tv.AdResponse; import android.media.tv.AitInfo; @@ -34,6 +35,8 @@ oneway interface ITvInputSessionCallback { void onSessionCreated(ITvInputSession session, in IBinder hardwareSessionToken); void onSessionEvent(in String name, in Bundle args); void onChannelRetuned(in Uri channelUri); + void onAudioPresentationsChanged(in List tvAudioPresentations); + void onAudioPresentationSelected(int presentationId, int programId); void onTracksChanged(in List tracks); void onTrackSelected(int type, in String trackId); void onVideoAvailable(); diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java index 634f1024ab199..fd846c56190bd 100644 --- a/media/java/android/media/tv/ITvInputSessionWrapper.java +++ b/media/java/android/media/tv/ITvInputSessionWrapper.java @@ -75,6 +75,7 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand private static final int DO_SET_IAPP_NOTIFICATION_ENABLED = 26; private static final int DO_REQUEST_AD = 27; private static final int DO_NOTIFY_AD_BUFFER = 28; + private static final int DO_SELECT_AUDIO_PRESENTATION = 29; private final boolean mIsRecordingSession; private final HandlerCaller mCaller; @@ -240,6 +241,12 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand mTvInputRecordingSessionImpl.resumeRecording((Bundle) msg.obj); break; } + case DO_SELECT_AUDIO_PRESENTATION: { + SomeArgs args = (SomeArgs) msg.obj; + mTvInputSessionImpl.selectAudioPresentation(args.argi1, args.argi2); + args.recycle(); + break; + } case DO_REQUEST_BROADCAST_INFO: { mTvInputSessionImpl.requestBroadcastInfo((BroadcastInfoRequest) msg.obj); break; @@ -275,8 +282,8 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand + "Consider handling the tune request in a separate thread."); } if (durationMs > EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS) { - throw new RuntimeException("Too much time to handle a request. (type=" + msg.what + - ", " + durationMs + "ms > " + EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS + "ms)."); + throw new RuntimeException("Too much time to handle a request. (type=" + msg.what + + ", " + durationMs + "ms > " + EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS + "ms)."); } } } @@ -322,6 +329,12 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_CAPTION_ENABLED, enabled)); } + @Override + public void selectAudioPresentation(int presentationId, int programId) { + mCaller.executeOrSendMessage(mCaller.obtainMessageII(DO_SELECT_AUDIO_PRESENTATION, + presentationId, programId)); + } + @Override public void selectTrack(int type, String trackId) { mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_SELECT_TRACK, type, trackId)); @@ -436,7 +449,7 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand } private final class TvInputEventReceiver extends InputEventReceiver { - public TvInputEventReceiver(InputChannel inputChannel, Looper looper) { + TvInputEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); } diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index e8127df8ee467..34da1784b5b0e 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.graphics.Rect; import android.media.AudioDeviceInfo; import android.media.AudioFormat.Encoding; +import android.media.AudioPresentation; import android.media.PlaybackParams; import android.media.tv.interactive.TvInteractiveAppManager; import android.net.Uri; @@ -549,6 +550,27 @@ public final class TvInputManager { public void onChannelRetuned(Session session, Uri channelUri) { } + /** + * This is called when the audio presentation information of the session has been changed. + * + * @param session A {@link TvInputManager.Session} associated with this callback. + * @param audioPresentations An updated list of selectable audio presentations. + */ + public void onAudioPresentationsChanged(Session session, + List audioPresentations) { + } + + /** + * This is called when an audio presentation is selected. + * + * @param session A {@link TvInputManager.Session} associated with this callback. + * @param presentationId The ID of the selected audio presentation. + * @param programId The ID of the program providing the selected audio presentation. + */ + public void onAudioPresentationSelected(Session session, int presentationId, + int programId) { + } + /** * This is called when the track information of the session has been changed. * @@ -777,6 +799,25 @@ public final class TvInputManager { }); } + void postAudioPresentationsChanged(final List audioPresentations) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onAudioPresentationsChanged(mSession, audioPresentations); + } + }); + } + + void postAudioPresentationSelected(final int presentationId, final int programId) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onAudioPresentationSelected(mSession, presentationId, + programId); + } + }); + } + void postTracksChanged(final List tracks) { mHandler.post(new Runnable() { @Override @@ -1234,6 +1275,36 @@ public final class TvInputManager { record.postChannelRetuned(channelUri); } } + @Override + public void onAudioPresentationsChanged(List audioPresentations, + int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + if (record.mSession.updateAudioPresentations(audioPresentations)) { + record.postAudioPresentationsChanged(audioPresentations); + } + } + } + + @Override + public void onAudioPresentationSelected(int presentationId, int programId, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + if (record.mSession.updateAudioPresentationSelection(presentationId, + programId)) { + record.postAudioPresentationSelected(presentationId, programId); + } + } + } + @Override public void onTracksChanged(List tracks, int seq) { @@ -2399,12 +2470,18 @@ public final class TvInputManager { private final Object mMetadataLock = new Object(); // @GuardedBy("mMetadataLock") + private final List mAudioPresentations = new ArrayList<>(); + // @GuardedBy("mMetadataLock") private final List mAudioTracks = new ArrayList<>(); // @GuardedBy("mMetadataLock") private final List mVideoTracks = new ArrayList<>(); // @GuardedBy("mMetadataLock") private final List mSubtitleTracks = new ArrayList<>(); // @GuardedBy("mMetadataLock") + private int mSelectedAudioProgramId = AudioPresentation.PROGRAM_ID_UNKNOWN; + // @GuardedBy("mMetadataLock") + private int mSelectedAudioPresentationId = AudioPresentation.PRESENTATION_ID_UNKNOWN; + // @GuardedBy("mMetadataLock") private String mSelectedAudioTrackId; // @GuardedBy("mMetadataLock") private String mSelectedVideoTrackId; @@ -2552,9 +2629,12 @@ public final class TvInputManager { return; } synchronized (mMetadataLock) { + mAudioPresentations.clear(); mAudioTracks.clear(); mVideoTracks.clear(); mSubtitleTracks.clear(); + mSelectedAudioProgramId = AudioPresentation.PROGRAM_ID_UNKNOWN; + mSelectedAudioPresentationId = AudioPresentation.PRESENTATION_ID_UNKNOWN; mSelectedAudioTrackId = null; mSelectedVideoTrackId = null; mSelectedSubtitleTrackId = null; @@ -2585,6 +2665,119 @@ public final class TvInputManager { } } + /** + * Selects an audio presentation + * + * @param presentationId The ID of the audio presentation to select. + * @param programId The ID of the program offering the selected audio presentation. + * @see #getAudioPresentations + */ + public void selectAudioPresentation(int presentationId, int programId) { + synchronized (mMetadataLock) { + if (presentationId != AudioPresentation.PRESENTATION_ID_UNKNOWN + && !containsAudioPresentation(mAudioPresentations, presentationId)) { + Log.w(TAG, "Invalid audio presentation id: " + presentationId); + return; + } + } + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.selectAudioPresentation(mToken, presentationId, programId, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private boolean containsAudioPresentation(List audioPresentations, + int presentationId) { + synchronized (mMetadataLock) { + for (AudioPresentation audioPresentation : audioPresentations) { + if (audioPresentation.getPresentationId() == presentationId) { + return true; + } + } + return false; + } + } + + /** + * Returns a list of audio presentations. + * + * @return the list of audio presentations. + * Returns empty AudioPresentation list if no presentations are available. + */ + public List getAudioPresentations() { + synchronized (mMetadataLock) { + if (mAudioPresentations == null) { + return new ArrayList(); + } + return new ArrayList(mAudioPresentations); + } + } + + /** + * Returns the program ID of the selected audio presentation. + * + * @return The ID of the program providing the selected audio presentation. + * Returns {@value AudioPresentation.PROGRAM_ID_UNKNOWN} if no audio presentation has + * been selected from a program. + * @see #selectAudioPresentation + */ + public int getSelectedProgramId() { + synchronized (mMetadataLock) { + return mSelectedAudioProgramId; + } + } + + /** + * Returns the presentation ID of the selected audio presentation. + * + * @return The ID of the selected audio presentation. + * Returns {@value AudioPresentation.PRESENTATION_ID_UNKNOWN} if no audio presentation + * has been selected. + * @see #selectAudioPresentation + */ + public int getSelectedAudioPresentationId() { + synchronized (mMetadataLock) { + return mSelectedAudioPresentationId; + } + } + + /** + * Responds to onAudioPresentationsChanged() and updates the internal audio presentation + * information. + * @return true if there is an update. + */ + boolean updateAudioPresentations(List audioPresentations) { + synchronized (mMetadataLock) { + mAudioPresentations.clear(); + for (AudioPresentation presentation : audioPresentations) { + mAudioPresentations.add(presentation); + } + return !mAudioPresentations.isEmpty(); + } + } + + /** + * Responds to onAudioPresentationSelected() and updates the internal audio presentation + * selection information. + * @return true if there is an update. + */ + boolean updateAudioPresentationSelection(int presentationId, int programId) { + synchronized (mMetadataLock) { + if ((programId != mSelectedAudioProgramId) + || (presentationId != mSelectedAudioPresentationId)) { + mSelectedAudioPresentationId = presentationId; + mSelectedAudioProgramId = programId; + return true; + } + } + return false; + } + /** * Selects a track. * diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java old mode 100755 new mode 100644 index 15f511b960e53..20b355150a208 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.graphics.PixelFormat; import android.graphics.Rect; import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.AudioPresentation; import android.media.PlaybackParams; import android.net.Uri; import android.os.AsyncTask; @@ -738,6 +739,74 @@ public abstract class TvInputService extends Service { }); } + /** + * Sends an updated list of all audio presentations available from a Next Generation Audio + * service. This is used by the framework to maintain the audio presentation information for + * a given track of {@link TvTrackInfo#TYPE_AUDIO}, which in turn is used by + * {@link TvView#getAudioPresentations} for the application to retrieve metadata for the + * current audio track. The TV input service must call this method as soon as the audio + * track presentation information becomes available or is updated. Note that in a case + * where a part of the information for the current track is updated, it is not necessary + * to create a new {@link TvTrackInfo} object with a different track ID. + * + * @param audioPresentations A list of audio presentation information pertaining to the + * selected track. + */ + public void notifyAudioPresentationChanged(@NonNull final List + audioPresentations) { + final List ap = new ArrayList<>(audioPresentations); + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "notifyAudioPresentationsChanged"); + } + if (mSessionCallback != null) { + mSessionCallback.onAudioPresentationsChanged(ap); + } + } catch (RemoteException e) { + Log.e(TAG, "error in notifyAudioPresentationsChanged", e); + } + } + }); + } + + /** + * Sends the presentation and program IDs of the selected audio presentation. This is used + * to inform the application that a specific audio presentation is selected. The TV input + * service must call this method as soon as an audio presentation is selected either by + * default or in response to a call to {@link #onSelectTrack}. The selected audio + * presentation ID for a currently selected audio track is maintained in the framework until + * the next call to this method even after the entire audio presentation list for the track + * is updated (but is reset when the session is tuned to a new channel), so care must be + * taken not to result in an obsolete track audio presentation ID. + * + * @param presentationId The ID of the selected audio presentation for the current track. + * @param programId The ID of the program providing the selected audio presentation. + * @see #onSelectAudioPresentation + */ + public void notifyAudioPresentationSelected(final int presentationId, final int programId) { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "notifyAudioPresentationSelected"); + } + if (mSessionCallback != null) { + mSessionCallback.onAudioPresentationSelected(presentationId, programId); + } + } catch (RemoteException e) { + Log.e(TAG, "error in notifyAudioPresentationSelected", e); + } + } + }); + } + + /** * Informs the application that the user is allowed to watch the current program content. * @@ -1245,6 +1314,23 @@ public abstract class TvInputService extends Service { public void onSetInteractiveAppNotificationEnabled(boolean enabled) { } + /** + * Selects an audio presentation. + * + *

On successfully selecting the audio presentation, + * {@link #notifyAudioPresentationSelected} is invoked to provide updated information about + * the selected audio presentation to applications. + * + * @param presentationId The ID of the audio presentation to select. + * @param programId The ID of the program providing the selected audio presentation. + * @return {@code true} if the audio presentation selection was successful, + * {@code false} otherwise. + * @see #notifyAudioPresentationSelected + */ + public boolean onSelectAudioPresentation(int presentationId, int programId) { + return false; + } + /** * Processes a private command sent from the application to the TV input. This can be used * to provide domain-specific features that are only known between certain TV inputs and @@ -1578,6 +1664,13 @@ public abstract class TvInputService extends Service { onSetCaptionEnabled(enabled); } + /** + * Calls {@link #onSelectAudioPresentation}. + */ + void selectAudioPresentation(int presentationId, int programId) { + onSelectAudioPresentation(presentationId, programId); + } + /** * Calls {@link #onSelectTrack}. */ diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java index 2fdbc3b29a655..c3e9cac2b4e3b 100644 --- a/media/java/android/media/tv/TvView.java +++ b/media/java/android/media/tv/TvView.java @@ -30,6 +30,7 @@ import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; +import android.media.AudioPresentation; import android.media.PlaybackParams; import android.media.tv.TvInputManager.Session; import android.media.tv.TvInputManager.Session.FinishedInputEventCallback; @@ -54,6 +55,7 @@ import android.view.ViewRootImpl; import java.lang.ref.WeakReference; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.List; import java.util.Queue; @@ -433,6 +435,35 @@ public class TvView extends ViewGroup { } } + /** + * Selects an audio presentation. + * + * @param presentationId The ID of the audio presentation to select. + * @param programId The ID of the program providing the selected audio presentation. + * @see #getAudioPresentations + */ + public void selectAudioPresentation(int presentationId, int programId) { + if (mSession != null) { + mSession.selectAudioPresentation(presentationId, programId); + } + } + + /** + * Returns the list of audio presentations from the selected track of type + * {@link TvTrackInfo#TYPE_AUDIO}. + * + * @return the list of audio presentations from the selected audio track, or an empty list if no + * audio presentations are available. + * @see #selectAudioPresentation + */ + @NonNull + public List getAudioPresentations() { + if (mSession == null) { + return new ArrayList(); + } + return mSession.getAudioPresentations(); + } + /** * Selects a track. * @@ -976,6 +1007,27 @@ public class TvView extends ViewGroup { public void onChannelRetuned(String inputId, Uri channelUri) { } + /** + * This is called when the audio presentation information has been changed. + * + * @param inputId The ID of the TV input bound to this view. + * @param audioPresentations A list of updated audio presentation information. + */ + public void onAudioPresentationsChanged(@NonNull String inputId, + @NonNull List audioPresentations) { + } + + /** + * This is called when audio presentation selection has changed. + * + * @param inputId The ID of the TV input bound to this view. + * @param presentationId The ID of the audio presentation selected. + * @param programId The ID of the program providing the selected audio presentation. + */ + public void onAudioPresentationSelected(@NonNull String inputId, int presentationId, + int programId) { + } + /** * This is called when the track information has been changed. * @@ -1241,6 +1293,37 @@ public class TvView extends ViewGroup { } } + @Override + public void onAudioPresentationsChanged(Session session, + List audioPresentations) { + if (DEBUG) { + Log.d(TAG, "onAudioPresentationsChanged(" + audioPresentations + ")"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onAudioPresentationsChanged - session not created"); + return; + } + if (mCallback != null) { + mCallback.onAudioPresentationsChanged(mInputId, audioPresentations); + } + } + + @Override + public void onAudioPresentationSelected(Session session, int presentationId, + int programId) { + if (DEBUG) { + Log.d(TAG, "onAudioPresentationSelected(presentationId=" + presentationId + + ", programId=" + programId + ")"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onAudioPresentationSelected - session not created"); + return; + } + if (mCallback != null) { + mCallback.onAudioPresentationSelected(mInputId, presentationId, programId); + } + } + @Override public void onTracksChanged(Session session, List tracks) { if (DEBUG) { diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java old mode 100755 new mode 100644 index 29b37cea8eebf..a03e4b6348aec --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -43,6 +43,7 @@ import android.content.pm.UserInfo; import android.graphics.Rect; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.AudioPresentation; import android.media.PlaybackParams; import android.media.tv.AdBuffer; import android.media.tv.AdRequest; @@ -1836,6 +1837,28 @@ public final class TvInputManagerService extends SystemService { } @Override + public void selectAudioPresentation(IBinder sessionToken, int presentationId, + int programId, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "selectAudioPresentation"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, + resolvedUserId).selectAudioPresentation( + presentationId, programId); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in selectAudioPresentation", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void selectTrack(IBinder sessionToken, int type, String trackId, int userId) { final int callingUid = Binder.getCallingUid(); final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, @@ -3359,6 +3382,43 @@ public final class TvInputManagerService extends SystemService { } } + @Override + public void onAudioPresentationsChanged(List audioPresentations) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onAudioPresentationChanged(" + audioPresentations + ")"); + } + if (mSessionState.session == null || mSessionState.client == null) { + return; + } + try { + mSessionState.client.onAudioPresentationsChanged(audioPresentations, + mSessionState.seq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onAudioPresentationsChanged", e); + } + } + } + + @Override + public void onAudioPresentationSelected(int presentationId, int programId) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onAudioPresentationSelected(presentationId=" + presentationId + + ", programId=" + programId + ")"); + } + if (mSessionState.session == null || mSessionState.client == null) { + return; + } + try { + mSessionState.client.onAudioPresentationSelected(presentationId, programId, + mSessionState.seq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onAudioPresentationSelected", e); + } + } + } + @Override public void onTracksChanged(List tracks) { synchronized (mLock) {