Merge "AudioTrack: support for offloaded playback"
diff --git a/core/jni/android_media_AudioFormat.h b/core/jni/android_media_AudioFormat.h
index 092aaf6..51cefb9 100644
--- a/core/jni/android_media_AudioFormat.h
+++ b/core/jni/android_media_AudioFormat.h
@@ -33,6 +33,8 @@
 #define ENCODING_AAC_HE_V2      12
 #define ENCODING_IEC61937       13
 #define ENCODING_DOLBY_TRUEHD   14
+#define ENCODING_AAC_ELD        15
+#define ENCODING_AAC_XHE        16
 
 #define ENCODING_INVALID    0
 #define ENCODING_DEFAULT    1
@@ -71,6 +73,10 @@
         return AUDIO_FORMAT_DOLBY_TRUEHD;
     case ENCODING_IEC61937:
         return AUDIO_FORMAT_IEC61937;
+    case ENCODING_AAC_ELD:
+        return AUDIO_FORMAT_AAC_ELD;
+    case ENCODING_AAC_XHE:
+        return AUDIO_FORMAT_AAC; // FIXME temporary value, needs addition of xHE-AAC
     case ENCODING_DEFAULT:
         return AUDIO_FORMAT_DEFAULT;
     default:
@@ -114,6 +120,11 @@
         return ENCODING_IEC61937;
     case AUDIO_FORMAT_DOLBY_TRUEHD:
         return ENCODING_DOLBY_TRUEHD;
+    case AUDIO_FORMAT_AAC_ELD:
+            return ENCODING_AAC_ELD;
+    // FIXME needs addition of AUDIO_FORMAT_AAC_XHE
+    //case AUDIO_FORMAT_AAC_XHE:
+    //    return ENCODING_AAC_XHE;
     case AUDIO_FORMAT_DEFAULT:
         return ENCODING_DEFAULT;
     default:
@@ -121,6 +132,25 @@
     }
 }
 
+// This function converts Java channel masks to a native channel mask.
+// validity should be checked with audio_is_output_channel().
+static inline audio_channel_mask_t nativeChannelMaskFromJavaChannelMasks(
+        jint channelPositionMask, jint channelIndexMask)
+{
+    // 0 is the java android.media.AudioFormat.CHANNEL_INVALID value
+    if (channelIndexMask != 0) {  // channel index mask takes priority
+        // To convert to a native channel mask, the Java channel index mask
+        // requires adding the index representation.
+        return audio_channel_mask_from_representation_and_bits(
+                        AUDIO_CHANNEL_REPRESENTATION_INDEX,
+                        channelIndexMask);
+    }
+    // To convert to a native channel mask, the Java channel position mask
+    // requires a shift by 2 to skip the two deprecated channel
+    // configurations "default" and "mono".
+    return (audio_channel_mask_t)((uint32_t)channelPositionMask >> 2);
+}
+
 static inline audio_channel_mask_t outChannelMaskToNative(int channelMask)
 {
     switch (channelMask) {
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 7ec68ed..2be9471 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -1770,6 +1770,24 @@
                                                   (audio_devices_t)device);
 }
 
+static jboolean
+android_media_AudioSystem_isOffloadSupported(JNIEnv *env, jobject thiz,
+        jint encoding, jint sampleRate, jint channelMask, jint channelIndexMask)
+{
+    audio_offload_info_t format = AUDIO_INFO_INITIALIZER;
+    format.format = (audio_format_t) audioFormatToNative(encoding);
+    format.sample_rate = (uint32_t) sampleRate;
+    format.channel_mask = nativeChannelMaskFromJavaChannelMasks(channelMask, channelIndexMask);
+    format.stream_type = AUDIO_STREAM_MUSIC;
+    format.has_video = false;
+    format.is_streaming = false;
+    // offload duration unknown at this point:
+    // client side code cannot access "audio.offload.min.duration.secs" property to make a query
+    // agnostic of duration, so using acceptable estimate of 2mn
+    format.duration_us = 120 * 1000000;
+    return AudioSystem::isOffloadSupported(format);
+}
+
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gMethods[] = {
@@ -1823,6 +1841,7 @@
                                     (void *)android_media_AudioSystem_registerRecordingCallback},
     {"systemReady", "()I", (void *)android_media_AudioSystem_systemReady},
     {"getStreamVolumeDB", "(III)F", (void *)android_media_AudioSystem_getStreamVolumeDB},
+    {"native_is_offload_supported", "(IIII)Z", (void *)android_media_AudioSystem_isOffloadSupported},
 };
 
 
diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp
index 556ac27..11011b1 100644
--- a/core/jni/android_media_AudioTrack.cpp
+++ b/core/jni/android_media_AudioTrack.cpp
@@ -73,6 +73,7 @@
     jobject     audioTrack_ref;
     bool        busy;
     Condition   cond;
+    bool        isOffload;
 };
 
 // keep these values in sync with AudioTrack.java
@@ -90,6 +91,7 @@
     AudioTrackJniStorage() {
         mCallbackData.audioTrack_class = 0;
         mCallbackData.audioTrack_ref = 0;
+        mCallbackData.isOffload = false;
     }
 
     ~AudioTrackJniStorage() {
@@ -132,27 +134,34 @@
     }
 
     switch (event) {
-    case AudioTrack::EVENT_MARKER: {
-        JNIEnv *env = AndroidRuntime::getJNIEnv();
-        if (user != NULL && env != NULL) {
-            env->CallStaticVoidMethod(
-                callbackInfo->audioTrack_class,
-                javaAudioTrackFields.postNativeEventInJava,
-                callbackInfo->audioTrack_ref, event, 0,0, NULL);
-            if (env->ExceptionCheck()) {
-                env->ExceptionDescribe();
-                env->ExceptionClear();
+    // Offload only events
+    case AudioTrack::EVENT_STREAM_END:
+    case AudioTrack::EVENT_MORE_DATA:
+    // a.k.a. tear down
+    case AudioTrack::EVENT_NEW_IAUDIOTRACK:
+        if (callbackInfo->isOffload) {
+            JNIEnv *env = AndroidRuntime::getJNIEnv();
+            if (user != NULL && env != NULL) {
+                env->CallStaticVoidMethod(
+                        callbackInfo->audioTrack_class,
+                        javaAudioTrackFields.postNativeEventInJava,
+                        callbackInfo->audioTrack_ref, event, 0,0, NULL);
+                if (env->ExceptionCheck()) {
+                    env->ExceptionDescribe();
+                    env->ExceptionClear();
+                }
             }
-        }
         } break;
 
+    // PCM and offload events
+    case AudioTrack::EVENT_MARKER:
     case AudioTrack::EVENT_NEW_POS: {
         JNIEnv *env = AndroidRuntime::getJNIEnv();
         if (user != NULL && env != NULL) {
             env->CallStaticVoidMethod(
-                callbackInfo->audioTrack_class,
-                javaAudioTrackFields.postNativeEventInJava,
-                callbackInfo->audioTrack_ref, event, 0,0, NULL);
+                    callbackInfo->audioTrack_class,
+                    javaAudioTrackFields.postNativeEventInJava,
+                    callbackInfo->audioTrack_ref, event, 0,0, NULL);
             if (env->ExceptionCheck()) {
                 env->ExceptionDescribe();
                 env->ExceptionClear();
@@ -198,30 +207,12 @@
     return getAudioTrack(env, audioTrackObj);
 }
 
-// This function converts Java channel masks to a native channel mask.
-// validity should be checked with audio_is_output_channel().
-static inline audio_channel_mask_t nativeChannelMaskFromJavaChannelMasks(
-        jint channelPositionMask, jint channelIndexMask)
-{
-    if (channelIndexMask != 0) {  // channel index mask takes priority
-        // To convert to a native channel mask, the Java channel index mask
-        // requires adding the index representation.
-        return audio_channel_mask_from_representation_and_bits(
-                        AUDIO_CHANNEL_REPRESENTATION_INDEX,
-                        channelIndexMask);
-    }
-    // To convert to a native channel mask, the Java channel position mask
-    // requires a shift by 2 to skip the two deprecated channel
-    // configurations "default" and "mono".
-    return (audio_channel_mask_t)(channelPositionMask >> 2);
-}
-
 // ----------------------------------------------------------------------------
 static jint
 android_media_AudioTrack_setup(JNIEnv *env, jobject thiz, jobject weak_this, jobject jaa,
         jintArray jSampleRate, jint channelPositionMask, jint channelIndexMask,
         jint audioFormat, jint buffSizeInBytes, jint memoryMode, jintArray jSession,
-        jlong nativeAudioTrack) {
+        jlong nativeAudioTrack, jboolean offload) {
 
     ALOGV("sampleRates=%p, channel mask=%x, index mask=%x, audioFormat(Java)=%d, buffSize=%d"
         "nativeAudioTrack=0x%" PRIX64,
@@ -322,8 +313,19 @@
         lpJniStorage->mCallbackData.audioTrack_class = (jclass)env->NewGlobalRef(clazz);
         // we use a weak reference so the AudioTrack object can be garbage collected.
         lpJniStorage->mCallbackData.audioTrack_ref = env->NewGlobalRef(weak_this);
+        lpJniStorage->mCallbackData.isOffload = offload;
         lpJniStorage->mCallbackData.busy = false;
 
+        audio_offload_info_t offloadInfo;
+        if (offload) {
+            offloadInfo = AUDIO_INFO_INITIALIZER;
+            offloadInfo.format = format;
+            offloadInfo.sample_rate = sampleRateInHertz;
+            offloadInfo.channel_mask = nativeChannelMask;
+            offloadInfo.has_video = false;
+            offloadInfo.stream_type = AUDIO_STREAM_MUSIC; //required for offload
+        }
+
         // initialize the native AudioTrack object
         status_t status = NO_ERROR;
         switch (memoryMode) {
@@ -342,7 +344,7 @@
                     true,// thread can call Java
                     sessionId,// audio session ID
                     AudioTrack::TRANSFER_SYNC,
-                    NULL,                         // default offloadInfo
+                    offload ? &offloadInfo : NULL,
                     -1, -1,                       // default uid, pid values
                     paa);
             break;
@@ -1234,7 +1236,7 @@
     {"native_stop",          "()V",      (void *)android_media_AudioTrack_stop},
     {"native_pause",         "()V",      (void *)android_media_AudioTrack_pause},
     {"native_flush",         "()V",      (void *)android_media_AudioTrack_flush},
-    {"native_setup",     "(Ljava/lang/Object;Ljava/lang/Object;[IIIIII[IJ)I",
+    {"native_setup",     "(Ljava/lang/Object;Ljava/lang/Object;[IIIIII[IJZ)I",
                                          (void *)android_media_AudioTrack_setup},
     {"native_finalize",      "()V",      (void *)android_media_AudioTrack_finalize},
     {"native_release",       "()V",      (void *)android_media_AudioTrack_release},
diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java
index 93fc3da..46fe89a 100644
--- a/media/java/android/media/AudioFormat.java
+++ b/media/java/android/media/AudioFormat.java
@@ -240,20 +240,25 @@
     public static final int ENCODING_DTS_HD = 8;
     /** Audio data format: MP3 compressed
      * @hide
+     * TODO unhide and add to @Encoding (intentional white space   
      * */
     public static final int ENCODING_MP3 = 9;
     /** Audio data format: AAC LC compressed
      * @hide
+     * TODO unhide and add to @Encoding (intentional white space   
      * */
     public static final int ENCODING_AAC_LC = 10;
     /** Audio data format: AAC HE V1 compressed
      * @hide
+     * TODO unhide and add to @Encoding (intentional white space   
      * */
     public static final int ENCODING_AAC_HE_V1 = 11;
     /** Audio data format: AAC HE V2 compressed
      * @hide
+     * TODO unhide and add to @Encoding (intentional white space   
      * */
     public static final int ENCODING_AAC_HE_V2 = 12;
+
     /** Audio data format: compressed audio wrapped in PCM for HDMI
      * or S/PDIF passthrough.
      * IEC61937 uses a stereo stream of 16-bit samples as the wrapper.
@@ -266,6 +271,16 @@
     /** Audio data format: DOLBY TRUEHD compressed
      **/
     public static final int ENCODING_DOLBY_TRUEHD = 14;
+    /** Audio data format: AAC ELD compressed
+     * @hide
+     * TODO unhide and add to @Encoding (intentional white space   
+     * */
+    public static final int ENCODING_AAC_ELD = 15;
+    /** Audio data format: AAC xHE compressed
+     * @hide
+     * TODO unhide and add to @Encoding (intentional white space   
+     * */
+    public static final int ENCODING_AAC_XHE = 16;
 
     /** @hide */
     public static String toLogFriendlyEncoding(int enc) {
@@ -298,6 +313,10 @@
                 return "ENCODING_IEC61937";
             case ENCODING_DOLBY_TRUEHD:
                 return "ENCODING_DOLBY_TRUEHD";
+            case ENCODING_AAC_ELD:
+                return "ENCODING_AAC_ELD";
+            case ENCODING_AAC_XHE:
+                return "ENCODING_AAC_XHE";
             default :
                 return "invalid encoding " + enc;
         }
@@ -514,6 +533,8 @@
         case ENCODING_AAC_HE_V1:
         case ENCODING_AAC_HE_V2:
         case ENCODING_IEC61937:
+        case ENCODING_AAC_ELD:
+        case ENCODING_AAC_XHE:
             return true;
         default:
             return false;
@@ -532,6 +553,13 @@
         case ENCODING_DTS:
         case ENCODING_DTS_HD:
         case ENCODING_IEC61937:
+            //TODO not true yet (intended white space     
+        case ENCODING_MP3:
+        case ENCODING_AAC_LC:
+        case ENCODING_AAC_HE_V1:
+        case ENCODING_AAC_HE_V2:
+        case ENCODING_AAC_ELD:
+        case ENCODING_AAC_XHE:
             return true;
         default:
             return false;
@@ -556,6 +584,8 @@
         case ENCODING_AAC_HE_V1:
         case ENCODING_AAC_HE_V2:
         case ENCODING_IEC61937: // wrapped in PCM but compressed
+        case ENCODING_AAC_ELD:
+        case ENCODING_AAC_XHE:
             return false;
         case ENCODING_INVALID:
         default:
@@ -581,6 +611,8 @@
         case ENCODING_AAC_LC:
         case ENCODING_AAC_HE_V1:
         case ENCODING_AAC_HE_V2:
+        case ENCODING_AAC_ELD:
+        case ENCODING_AAC_XHE:
             return false;
         case ENCODING_INVALID:
         default:
@@ -794,14 +826,7 @@
 
         /**
          * Sets the data encoding format.
-         * @param encoding one of {@link AudioFormat#ENCODING_DEFAULT},
-         *     {@link AudioFormat#ENCODING_PCM_8BIT},
-         *     {@link AudioFormat#ENCODING_PCM_16BIT},
-         *     {@link AudioFormat#ENCODING_PCM_FLOAT},
-         *     {@link AudioFormat#ENCODING_AC3},
-         *     {@link AudioFormat#ENCODING_E_AC3}.
-         *     {@link AudioFormat#ENCODING_DTS},
-         *     {@link AudioFormat#ENCODING_DTS_HD}.
+         * @param encoding the specified encoding or default.
          * @return the same Builder instance.
          * @throws java.lang.IllegalArgumentException
          */
@@ -818,6 +843,12 @@
                 case ENCODING_DTS:
                 case ENCODING_DTS_HD:
                 case ENCODING_IEC61937:
+                case ENCODING_MP3:
+                case ENCODING_AAC_LC:
+                case ENCODING_AAC_HE_V1:
+                case ENCODING_AAC_HE_V2:
+                case ENCODING_AAC_ELD:
+                case ENCODING_AAC_XHE:
                     mEncoding = encoding;
                     break;
                 case ENCODING_INVALID:
@@ -1016,7 +1047,7 @@
     }
 
     /** @hide */
-    @IntDef({
+    @IntDef(flag = false, prefix = "ENCODING", value = {
         ENCODING_DEFAULT,
         ENCODING_PCM_8BIT,
         ENCODING_PCM_16BIT,
@@ -1025,8 +1056,8 @@
         ENCODING_E_AC3,
         ENCODING_DTS,
         ENCODING_DTS_HD,
-        ENCODING_IEC61937
-    })
+        ENCODING_IEC61937 }
+    )
     @Retention(RetentionPolicy.SOURCE)
     public @interface Encoding {}
 
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 913b5e8..3b77aa1 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1329,6 +1329,21 @@
      }
 
     //====================================================================
+    // Offload query
+    /**
+     * @hide
+     * TODO unhide (intentional white space to attract attention:    
+     * Returns whether offloaded playback of an audio format is supported on the device.
+     * Offloaded playback is where the decoding of an audio stream is not competing with other
+     * software resources. In general, it is supported by dedicated hardware, such as audio DSPs.
+     * @param format the audio format (codec, sample rate, channels) being checked.
+     * @return true if the given audio format can be offloaded.
+     */
+    public static boolean isOffloadedPlaybackSupported(@NonNull AudioFormat format) {
+        return AudioSystem.isOffloadSupported(format);
+    }
+
+    //====================================================================
     // Bluetooth SCO control
     /**
      * Sticky broadcast intent action indicating that the Bluetooth SCO audio
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 2cd764d..b4316ba 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -16,6 +16,7 @@
 
 package android.media;
 
+import android.annotation.NonNull;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.audiopolicy.AudioMix;
@@ -818,6 +819,14 @@
 
     public static native float getStreamVolumeDB(int stream, int index, int device);
 
+    static boolean isOffloadSupported(@NonNull AudioFormat format) {
+        return native_is_offload_supported(format.getEncoding(), format.getSampleRate(),
+                format.getChannelMask(), format.getChannelIndexMask());
+    }
+
+    private static native boolean native_is_offload_supported(int encoding, int sampleRate,
+            int channelMask, int channelIndexMask);
+
     // Items shared with audio service
 
     /**
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index e535fdf..6add381 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -24,7 +24,9 @@
 import java.nio.ByteOrder;
 import java.nio.NioUtils;
 import java.util.Collection;
+import java.util.concurrent.Executor;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -185,6 +187,22 @@
      * Event id denotes when previously set update period has elapsed during playback.
      */
     private static final int NATIVE_EVENT_NEW_POS = 4;
+    /**
+     * Callback for more data
+     * TODO only for offload
+     */
+    private static final int NATIVE_EVENT_MORE_DATA = 0;
+    /**
+     * IAudioTrack tear down for offloaded tracks
+     * TODO: when received, java AudioTrack must be released
+     */
+    private static final int NATIVE_EVENT_NEW_IAUDIOTRACK = 6;
+    /**
+     * Event id denotes when all the buffers queued in AF and HW are played
+     * back (after stop is called) for an offloaded track.
+     * TODO: not just for offload
+     */
+    private static final int NATIVE_EVENT_STREAM_END = 7;
 
     private final static String TAG = "android.media.AudioTrack";
 
@@ -540,6 +558,12 @@
     public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
             int mode, int sessionId)
                     throws IllegalArgumentException {
+        this(attributes, format, bufferSizeInBytes, mode, sessionId, false /*offload*/);
+    }
+
+    private AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
+            int mode, int sessionId, boolean offload)
+                    throws IllegalArgumentException {
         super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK);
         // mState already == STATE_UNINITIALIZED
 
@@ -601,7 +625,8 @@
         // native initialization
         int initResult = native_setup(new WeakReference<AudioTrack>(this), mAttributes,
                 sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat,
-                mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/);
+                mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/,
+                offload);
         if (initResult != SUCCESS) {
             loge("Error code "+initResult+" when initializing AudioTrack.");
             return; // with mState == STATE_UNINITIALIZED
@@ -681,7 +706,8 @@
                     0 /*mNativeBufferSizeInBytes - NA*/,
                     0 /*mDataLoadMode - NA*/,
                     session,
-                    nativeTrackInJavaObj);
+                    nativeTrackInJavaObj,
+                    false /*offload*/);
             if (initResult != SUCCESS) {
                 loge("Error code "+initResult+" when initializing AudioTrack.");
                 return; // with mState == STATE_UNINITIALIZED
@@ -730,6 +756,7 @@
      * <br>If the session ID is not specified with {@link #setSessionId(int)}, a new one will
      * be generated.
      */
+    // TODO add that offload is false by default (intended white space   
     public static class Builder {
         private AudioAttributes mAttributes;
         private AudioFormat mFormat;
@@ -737,6 +764,7 @@
         private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
         private int mMode = MODE_STREAM;
         private int mPerformanceMode = PERFORMANCE_MODE_NONE;
+        private boolean mOffload = false;
 
         /**
          * Constructs a new Builder with the default values as described above.
@@ -867,6 +895,24 @@
         }
 
         /**
+         * @hide
+         * TODO unhide (intentional whitespace    
+         * TODO should offload require POWER_SAVING?    
+         * Sets whether this track will play through the offloaded audio path.
+         * When set to true, at build time, the audio format will be checked against
+         * {@link AudioManager#isOffloadedPlaybackSupported(AudioFormat)} to verify the audio format
+         * used by this track is supported on the device's offload path (if any).
+         * <br>Offload is only supported for media audio data, and therefore require that
+         * the usage be {@link AudioAttributes#USAGE_MEDIA}.
+         * @param offload true to require the offload path for playback.
+         * @return the same Builder instance.
+         */
+        public @NonNull Builder setOffloadedPlayback(boolean offload) {
+            mOffload = offload;
+            return this;
+        }
+
+        /**
          * Builds an {@link AudioTrack} instance initialized with all the parameters set
          * on this <code>Builder</code>.
          * @return a new successfully initialized {@link AudioTrack} instance.
@@ -909,6 +955,19 @@
                         .setEncoding(AudioFormat.ENCODING_DEFAULT)
                         .build();
             }
+
+            //TODO tie offload to PERFORMANCE_MODE_POWER_SAVING?    
+            if (mOffload) {
+                if (mAttributes.getUsage() != AudioAttributes.USAGE_MEDIA) {
+                    throw new UnsupportedOperationException(
+                            "Cannot create AudioTrack, offload requires USAGE_MEDIA");
+                }
+                if (!AudioManager.isOffloadedPlaybackSupported(mFormat)) {
+                    throw new UnsupportedOperationException(
+                            "Cannot create AudioTrack, offload format not supported");
+                }
+            }
+
             try {
                 // If the buffer size is not specified in streaming mode,
                 // use a single frame for the buffer size and let the
@@ -918,7 +977,7 @@
                             * mFormat.getBytesPerSample(mFormat.getEncoding());
                 }
                 final AudioTrack track = new AudioTrack(
-                        mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId);
+                        mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId, mOffload);
                 if (track.getState() == STATE_UNINITIALIZED) {
                     // release is not necessary
                     throw new UnsupportedOperationException("Cannot create AudioTrack");
@@ -2882,6 +2941,55 @@
         void onPeriodicNotification(AudioTrack track);
     }
 
+    /**
+     * @hide
+     * TODO unhide (intentional white space to attract attention:    
+     * Abstract class to receive event notification about the stream playback.
+     */
+    public abstract static class StreamEventCallback {
+        // TODO rename if supported for non offload tracks
+        public void onTearDown(AudioTrack track) { }
+        public void onStreamPresentationEnd(AudioTrack track) { }
+        public void onStreamDataRequest(AudioTrack track) { }
+    }
+
+    private Executor mStreamEventExec;
+    private StreamEventCallback mStreamEventCb;
+    private final Object mStreamEventCbLock = new Object();
+
+    /**
+     * @hide
+     * TODO unhide (intentional white space to attract attention:    
+     * Registers a callback for notification of stream events.
+     * @param executor {@link Executor} to handle the callbacks
+     * @param eventCallback the callback to receive the stream events
+     */
+    public void setStreamEventCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull StreamEventCallback eventCallback) {
+        if (eventCallback == null) {
+            throw new IllegalArgumentException("Illegal null StreamEventCallback");
+        }
+        if (executor == null) {
+            throw new IllegalArgumentException("Illegal null Executor for the StreamEventCallback");
+        }
+        synchronized (mStreamEventCbLock) {
+            mStreamEventExec = executor;
+            mStreamEventCb = eventCallback;
+        }
+    }
+
+    /**
+     * @hide
+     * Unregisters the callback for notification of stream events, previously set
+     * by {@link #setStreamEventCallback(StreamEventCallback, Executor)}.
+     */
+    public void removeStreamEventCallback() {
+        synchronized (mStreamEventCbLock) {
+            mStreamEventExec = null;
+            mStreamEventCb = null;
+        }
+    }
+
     //---------------------------------------------------------
     // Inner classes
     //--------------------
@@ -2965,7 +3073,7 @@
     private static void postEventFromNative(Object audiotrack_ref,
             int what, int arg1, int arg2, Object obj) {
         //logd("Event posted from the native side: event="+ what + " args="+ arg1+" "+arg2);
-        AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get();
+        final AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get();
         if (track == null) {
             return;
         }
@@ -2974,6 +3082,32 @@
             track.broadcastRoutingChange();
             return;
         }
+
+        if (what == NATIVE_EVENT_MORE_DATA || what == NATIVE_EVENT_NEW_IAUDIOTRACK
+                || what == NATIVE_EVENT_STREAM_END) {
+            final Executor exec;
+            final StreamEventCallback cb;
+            synchronized (track.mStreamEventCbLock) {
+                exec = track.mStreamEventExec;
+                cb = track.mStreamEventCb;
+            }
+            if ((exec == null) || (cb == null)) {
+                return;
+            }
+            switch (what) {
+                case NATIVE_EVENT_MORE_DATA:
+                    exec.execute(() -> cb.onStreamDataRequest(track));
+                    return;
+                case NATIVE_EVENT_NEW_IAUDIOTRACK:
+                    // TODO also release track as it's not longer usable
+                    exec.execute(() -> cb.onTearDown(track));
+                    return;
+                case NATIVE_EVENT_STREAM_END:
+                    exec.execute(() -> cb.onStreamPresentationEnd(track));
+                    return;
+            }
+        }
+
         NativePositionEventHandlerDelegate delegate = track.mEventHandlerDelegate;
         if (delegate != null) {
             Handler handler = delegate.getHandler();
@@ -2995,7 +3129,8 @@
     private native final int native_setup(Object /*WeakReference<AudioTrack>*/ audiotrack_this,
             Object /*AudioAttributes*/ attributes,
             int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
-            int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack);
+            int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack,
+            boolean offload);
 
     private native final void native_finalize();