oboe: add AudioStreamDataCallback and AudioStreamErrorCallback

This allows an app to use different callbacks for data processing
and error processing. The AudioStreamCallback inherits
from both interfaces.

There is also a new method: bool onError()
that allows an app to completely override the default
error handling.

This change is backwards compatible with older versions.

Fixes #917
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index ea8b122..7809206 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -130,7 +130,7 @@
     if (mUseCallback) {
         LOGD("ActivityContext::open() set callback to use oboeCallbackProxy, callback size = %d",
              callbackSize);
-        builder.setCallback(&oboeCallbackProxy);
+        builder.setDataCallback(&oboeCallbackProxy);
         builder.setFramesPerCallback(callbackSize);
     }
 }
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index 50d0e90..95df984 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -192,6 +192,7 @@
 public:
 
     ActivityContext() {}
+
     virtual ~ActivityContext() = default;
 
     oboe::AudioStream *getStream(int32_t streamIndex) {
@@ -288,7 +289,11 @@
     }
 
     oboe::Result getLastErrorCallbackResult() {
-        return oboeCallbackProxy.getLastErrorCallbackResult();
+        oboe::AudioStream *stream = getOutputStream();
+        if (stream == nullptr) {
+            stream = getInputStream();
+        }
+        return stream ? oboe::Result::ErrorNull : stream->getLastErrorCallbackResult();
     }
 
     int32_t getFramesPerCallback() {
diff --git a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp
index f16e861..a17575b 100644
--- a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp
+++ b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp
@@ -80,17 +80,18 @@
     return callbackResult;
 }
 
-void OboeStreamCallbackProxy::onErrorBeforeClose(oboe::AudioStream *audioStream, oboe::Result error) {
-    LOGD("OboeStreamCallbackProxy::%s(%p, %d) called", __func__, audioStream, error);
-    mErrorCallbackResult = error;
-    if (mCallback != nullptr) {
-        mCallback->onErrorBeforeClose(audioStream, error);
-    }
-}
-
-void OboeStreamCallbackProxy::onErrorAfterClose(oboe::AudioStream *audioStream, oboe::Result  error) {
-    LOGD("OboeStreamCallbackProxy::%s(%p, %d) called", __func__, audioStream, error);
-    if (mCallback != nullptr) {
-        mCallback->onErrorAfterClose(audioStream, error);
-    }
-}
+// FIXME
+//void OboeStreamCallbackProxy::onErrorBeforeClose(oboe::AudioStream *audioStream, oboe::Result error) {
+//    LOGD("OboeStreamCallbackProxy::%s(%p, %d) called", __func__, audioStream, error);
+//    mErrorCallbackResult = error;
+//    if (mCallback != nullptr) {
+//        mCallback->onErrorBeforeClose(audioStream, error);
+//    }
+//}
+//
+//void OboeStreamCallbackProxy::onErrorAfterClose(oboe::AudioStream *audioStream, oboe::Result  error) {
+//    LOGD("OboeStreamCallbackProxy::%s(%p, %d) called", __func__, audioStream, error);
+//    if (mCallback != nullptr) {
+//        mCallback->onErrorAfterClose(audioStream, error);
+//    }
+//}
diff --git a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h
index e9cd4fe..9bfeaca 100644
--- a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h
+++ b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h
@@ -22,7 +22,7 @@
 
 #include "oboe/Oboe.h"
 
-class OboeStreamCallbackProxy : public oboe::AudioStreamCallback {
+class OboeStreamCallbackProxy : public oboe::AudioStreamDataCallback {
 public:
 
     void setCallback(oboe::AudioStreamCallback *callback) {
@@ -54,10 +54,6 @@
             void *audioData,
             int numFrames) override;
 
-    void onErrorBeforeClose(oboe::AudioStream *audioStream, oboe::Result error) override;
-
-    void onErrorAfterClose(oboe::AudioStream *audioStream, oboe::Result error) override;
-
     /**
      * Specify the amount of artificial workload that will waste CPU cycles
      * and increase the CPU load.
@@ -77,10 +73,6 @@
 
     static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC);
 
-    oboe::Result getLastErrorCallbackResult() {
-        return mErrorCallbackResult;
-    }
-
 private:
     static constexpr int32_t   kWorkloadScaler = 500;
     double                     mWorkload = 0.0;
@@ -90,7 +82,6 @@
     static bool                mCallbackReturnStop;
     int64_t                    mCallbackCount = 0;
     std::atomic<int32_t>       mFramesPerCallback{0};
-    oboe::Result               mErrorCallbackResult = oboe::Result::OK;
 };
 
 
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index 067db81..09c273f 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -381,8 +381,12 @@
 
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getLastErrorCallbackResult(
-        JNIEnv *env, jobject) {
-    return (jint) engine.getCurrentActivity()->getLastErrorCallbackResult();
+        JNIEnv *env, jobject, jint streamIndex) {
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+    if (oboeStream != nullptr) {
+        return (jint) oboeStream->getLastErrorCallbackResult();
+    }
+    return 0;
 }
 
 JNIEXPORT jdouble JNICALL
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java
index d13a85c..32462f1 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java
@@ -204,7 +204,10 @@
     public native long getCallbackCount(); // TODO Move to another class?
 
     @Override
-    public native int getLastErrorCallbackResult(); // TODO Move to another class?
+    public int getLastErrorCallbackResult() {
+        return getLastErrorCallbackResult(streamIndex);
+    }
+    public native int getLastErrorCallbackResult(int streamIndex);
 
     @Override
     public long getFramesWritten() {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java
index aa94404..ce9a5f4 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java
@@ -262,13 +262,14 @@
         return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN")
                 + ", Perf = " + StreamConfiguration.convertPerformanceModeToText(
                 config.getPerformanceMode())
-                + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode());
+                + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode())
+                + ", " + config.getSampleRate();
     }
 
     private void testConfiguration(boolean isInput,
                                    int perfMode,
                                    int sharingMode,
-                                   int channelCount,
+                                   int sampleRate,
                                    boolean requestPlugin) throws InterruptedException {
         String actualConfigText = "none";
         mSkipTest = false;
@@ -295,7 +296,10 @@
         requestedConfig.reset();
         requestedConfig.setPerformanceMode(perfMode);
         requestedConfig.setSharingMode(sharingMode);
-        requestedConfig.setChannelCount(channelCount);
+        requestedConfig.setSampleRate(sampleRate);
+        if (sampleRate != 0) {
+            requestedConfig.setRateConversionQuality(StreamConfiguration.RATE_CONVERSION_QUALITY_MEDIUM);
+        }
 
         log("========================== #" + mTestCount);
         log("Requested:");
@@ -427,12 +431,17 @@
     }
 
     private void testConfiguration(boolean isInput, int performanceMode,
-                                   int sharingMode) throws InterruptedException {
-        int channelCount = 2;
+                                   int sharingMode, int sampleRate) throws InterruptedException {
         boolean requestPlugin = true; // plug IN
-        testConfiguration(isInput, performanceMode, sharingMode, channelCount, requestPlugin);
+        testConfiguration(isInput, performanceMode, sharingMode, sampleRate, requestPlugin);
         requestPlugin = false; // UNplug
-        testConfiguration(isInput, performanceMode, sharingMode, channelCount, requestPlugin);
+        testConfiguration(isInput, performanceMode, sharingMode, sampleRate, requestPlugin);
+    }
+
+    private void testConfiguration(boolean isInput, int performanceMode,
+                                   int sharingMode) throws InterruptedException {
+        final int sampleRate = 0;
+        testConfiguration(isInput, performanceMode, sharingMode, sampleRate);
     }
 
     private void testConfiguration(int performanceMode,
@@ -454,6 +463,8 @@
         mFailCount = 0;
         // Try several different configurations.
         try {
+            testConfiguration(false, StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
+                    StreamConfiguration.SHARING_MODE_EXCLUSIVE, 44100);
             testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
                         StreamConfiguration.SHARING_MODE_EXCLUSIVE);
             testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
diff --git a/include/oboe/AudioStream.h b/include/oboe/AudioStream.h
index a158aaf..c862de6 100644
--- a/include/oboe/AudioStream.h
+++ b/include/oboe/AudioStream.h
@@ -393,12 +393,25 @@
      * Swap old callback for new callback.
      * This not atomic.
      * This should only be used internally.
-     * @param streamCallback
-     * @return previous streamCallback
+     * @param dataCallback
+     * @return previous dataCallback
      */
-    AudioStreamCallback *swapCallback(AudioStreamCallback *streamCallback) {
-        AudioStreamCallback *previousCallback = mStreamCallback;
-        mStreamCallback = streamCallback;
+    AudioStreamDataCallback *swapDataCallback(AudioStreamDataCallback *dataCallback) {
+        AudioStreamDataCallback *previousCallback = mDataCallback;
+        mDataCallback = dataCallback;
+        return previousCallback;
+    }
+
+    /*
+     * Swap old callback for new callback.
+     * This not atomic.
+     * This should only be used internally.
+     * @param errorCallback
+     * @return previous errorCallback
+     */
+    AudioStreamErrorCallback *swapErrorCallback(AudioStreamErrorCallback *errorCallback) {
+        AudioStreamErrorCallback *previousCallback = mErrorCallback;
+        mErrorCallback = errorCallback;
         return previousCallback;
     }
 
@@ -419,6 +432,13 @@
     ResultWithValue<int32_t> waitForAvailableFrames(int32_t numFrames,
                                                     int64_t timeoutNanoseconds);
 
+    /**
+     * @return last result passed from an error callback
+     */
+    virtual oboe::Result getLastErrorCallbackResult() const {
+        return mErrorCallbackResult;
+    }
+
 protected:
 
     /**
@@ -515,8 +535,10 @@
 
     std::mutex           mLock; // for synchronizing start/stop/close
 
+    oboe::Result         mErrorCallbackResult = oboe::Result::OK;
 
 private:
+
     // Log the scheduler if it changes.
     void                 checkScheduler();
     int                  mPreviousScheduler = -1;
diff --git a/include/oboe/AudioStreamBase.h b/include/oboe/AudioStreamBase.h
index 787ff20..73c27df 100644
--- a/include/oboe/AudioStreamBase.h
+++ b/include/oboe/AudioStreamBase.h
@@ -100,10 +100,35 @@
     int32_t getDeviceId() const { return mDeviceId; }
 
     /**
-     * @return the callback object for this stream, if set.
+     * For internal use only.
+     * @return the data callback object for this stream, if set.
      */
-    AudioStreamCallback* getCallback() const {
-        return mStreamCallback;
+    AudioStreamDataCallback *getDataCallback() const {
+        return mDataCallback;
+    }
+
+    /**
+     * For internal use only.
+     * @return the error callback object for this stream, if set.
+     */
+    AudioStreamErrorCallback *getErrorCallback() const {
+        return mErrorCallback;
+    }
+
+    /**
+     * @return true if a data callback was set for this stream
+     */
+    bool isDataCallbackSpecified() const {
+        return mDataCallback != nullptr;
+    }
+
+    /**
+     * Note that if the app does not set an error callback then a
+     * default one may be provided.
+     * @return true if an error callback was set for this stream
+     */
+    bool isErrorCallbackSpecified() const {
+        return mErrorCallback != nullptr;
     }
 
     /**
@@ -148,9 +173,15 @@
     }
 
 protected:
+    /** The callback which will be fired when new data is ready to be read/written. **/
+    AudioStreamDataCallback        *mDataCallback = nullptr;
 
-    /** The callback which will be fired when new data is ready to be read/written **/
-    AudioStreamCallback            *mStreamCallback = nullptr;
+    /** The callback which will be fired when an error or a disconnect occurs. **/
+    AudioStreamErrorCallback       *mErrorCallback = nullptr;
+
+    /** The callback that combines data and error callback. **/
+    // AudioStreamCallback            *mStreamCallback = nullptr;
+
     /** Number of audio frames which will be requested in each callback */
     int32_t                         mFramesPerCallback = kUnspecified;
     /** Stream channel count */
diff --git a/include/oboe/AudioStreamBuilder.h b/include/oboe/AudioStreamBuilder.h
index 80c5da4..d63578c 100644
--- a/include/oboe/AudioStreamBuilder.h
+++ b/include/oboe/AudioStreamBuilder.h
@@ -306,8 +306,44 @@
     }
 
     /**
+     * Specifies an object to handle data related callbacks from the underlying API.
+     *
+     * <strong>Important: See AudioStreamCallback for restrictions on what may be called
+     * from the callback methods.</strong>
+     *
+     * @param dataCallback
+     * @return pointer to the builder so calls can be chained
+     */
+    AudioStreamBuilder *setDataCallback(oboe::AudioStreamDataCallback *dataCallback) {
+        mDataCallback = dataCallback;
+        return this;
+    }
+
+    /**
+     * Specifies an object to handle error related callbacks from the underlying API.
+     * This can occur when a stream is disconnected because a headset is plugged in or unplugged.
+     * It can also occur if the audio service fails or if an exclusive stream is stolen by
+     * another stream.
+     *
+     * <strong>Important: See AudioStreamCallback for restrictions on what may be called
+     * from the callback methods.</strong>
+     *
+     * <strong>When an error callback occurs, the associated stream must be stopped and closed
+     * in a separate thread.</strong>
+     *
+     * @param errorCallback
+     * @return pointer to the builder so calls can be chained
+     */
+    AudioStreamBuilder *setErrorCallback(oboe::AudioStreamErrorCallback *errorCallback) {
+        mErrorCallback = errorCallback;
+        return this;
+    }
+
+    /**
      * Specifies an object to handle data or error related callbacks from the underlying API.
      *
+     * This is the equivalent of calling both setDataCallback() and setErrorCallback().
+     *
      * <strong>Important: See AudioStreamCallback for restrictions on what may be called
      * from the callback methods.</strong>
      *
@@ -327,7 +363,9 @@
      * @return pointer to the builder so calls can be chained
      */
     AudioStreamBuilder *setCallback(AudioStreamCallback *streamCallback) {
-        mStreamCallback = streamCallback;
+        // Use the same callback object for both, dual inheritance.
+        mDataCallback = streamCallback;
+        mErrorCallback = streamCallback;
         return this;
     }
 
diff --git a/include/oboe/AudioStreamCallback.h b/include/oboe/AudioStreamCallback.h
index f427693..0018623 100644
--- a/include/oboe/AudioStreamCallback.h
+++ b/include/oboe/AudioStreamCallback.h
@@ -24,15 +24,16 @@
 class AudioStream;
 
 /**
- * AudioStreamCallback defines a callback interface for:
- *
- * 1) moving data to/from an audio stream using `onAudioReady`
+ * AudioStreamDataCallback defines a callback interface for
+ * moving data to/from an audio stream using `onAudioReady`
  * 2) being alerted when a stream has an error using `onError*` methods
  *
+ * It is used with AudioStreamBuilder::setDataCallback().
  */
-class AudioStreamCallback {
+
+class AudioStreamDataCallback {
 public:
-    virtual ~AudioStreamCallback() = default;
+    virtual ~AudioStreamDataCallback() = default;
 
     /**
      * A buffer is ready for processing.
@@ -75,21 +76,60 @@
      * If you need to move data, eg. MIDI commands, in or out of the callback function then
      * we recommend the use of non-blocking techniques such as an atomic FIFO.
      *
-     * @param oboeStream pointer to the associated stream
+     * @param audioStream pointer to the associated stream
      * @param audioData buffer containing input data or a place to put output data
      * @param numFrames number of frames to be processed
      * @return DataCallbackResult::Continue or DataCallbackResult::Stop
      */
     virtual DataCallbackResult onAudioReady(
-            AudioStream *oboeStream,
+            AudioStream *audioStream,
             void *audioData,
             int32_t numFrames) = 0;
+};
+
+/**
+ * AudioStreamDataCallback defines a callback interface for
+ * being alerted when a stream has an error or is disconnected
+ * using `onError*` methods.
+ *
+ * It is used with AudioStreamBuilder::setErrorCallback().
+ */
+class AudioStreamErrorCallback {
+public:
+    virtual ~AudioStreamErrorCallback() = default;
 
     /**
-     * This will be called when an error occurs on a stream or when the stream is disconnected.
+     * This will be called first when an error occurs on a stream or when the stream is disconnected.
      *
-     * Note that this will be called on a different thread than the onAudioReady() thread.
-     * This thread will be created by Oboe.
+     * It can be used to override and customize the normal error processing.
+     * Use of this method is considered an advanced technique.
+     * It might, for example, be used if an app want to use a high level lock when
+     * closing and reopening  a stream.
+     * Or it might be used when an app want to signal a management thread that handles
+     * all of the stream state.
+     *
+     * If this method returns false then
+     * the stream will be stopped, and onErrorBeforeClose() will be called,
+     * then the stream will be closed and onErrorAfterClose() will be closed.
+     *
+     * If this method returns true then the normal error processing will not occur.
+     * In that case, the app MUST stop() and close() the stream!
+     *
+     * Note that this will be called on a thread created by Oboe.
+     *
+     * @param audioStream pointer to the associated stream
+     * @param error
+     */
+    virtual bool onError(AudioStream* /* audioStream */, Result /* error */) {
+        return false; // false means the stream will be stopped and closed by Oboe
+        // return true; // true means the stream will be stopped and closed by the app
+    }
+
+    /**
+     * This will be called when an error occurs on a stream or when the stream is disconnected
+     * and if onError() returns false.
+     *
+     * Note that this will be called on a thread created by Oboe.
      *
      * The underlying stream will already be stopped by Oboe but not yet closed.
      * So the stream can be queried.
@@ -97,27 +137,45 @@
      * Do not close or delete the stream in this method because it will be
      * closed after this method returns.
      *
-     * @param oboeStream pointer to the associated stream
+     * @param audioStream pointer to the associated stream
      * @param error
      */
-    virtual void onErrorBeforeClose(AudioStream* /* oboeStream */, Result /* error */) {}
+    virtual void onErrorBeforeClose(AudioStream* /* audioStream */, Result /* error */) {}
 
     /**
-     * This will be called when an error occurs on a stream or when the stream is disconnected.
+     * This will be called when an error occurs on a stream or when the stream is disconnected
+     * and if onError() returns false.
+     *
      * The underlying AAudio or OpenSL ES stream will already be stopped AND closed by Oboe.
      * So the underlying stream cannot be referenced.
      * But you can still query most parameters.
      *
      * This callback could be used to reopen a new stream on another device.
-     * You can safely delete the old AudioStream in this method.
      *
-     * @param oboeStream pointer to the associated stream
+     * @param audioStream pointer to the associated stream
      * @param error
      */
-    virtual void onErrorAfterClose(AudioStream* /* oboeStream */, Result /* error */) {}
+    virtual void onErrorAfterClose(AudioStream* /* audioStream */, Result /* error */) {}
 
 };
 
+/**
+ * AudioStreamCallback defines a callback interface for:
+ *
+ * 1) moving data to/from an audio stream using `onAudioReady`
+ * 2) being alerted when a stream has an error using `onError*` methods
+ *
+ * It is used with AudioStreamBuilder::setCallback().
+ *
+ * It combines the interfaces defined by AudioStreamDataCallback and AudioStreamErrorCallback.
+ * This was the original callback object. We now recommend using the individual interfaces.
+ */
+class AudioStreamCallback : public AudioStreamDataCallback,
+                            public AudioStreamErrorCallback {
+public:
+    virtual ~AudioStreamCallback() = default;
+};
+
 } // namespace oboe
 
 #endif //OBOE_STREAM_CALLBACK_H
diff --git a/src/aaudio/AudioStreamAAudio.cpp b/src/aaudio/AudioStreamAAudio.cpp
index 6d77c50..0b7a2e8 100644
--- a/src/aaudio/AudioStreamAAudio.cpp
+++ b/src/aaudio/AudioStreamAAudio.cpp
@@ -62,14 +62,16 @@
 static void oboe_aaudio_error_thread_proc(AudioStreamAAudio *oboeStream,
                                           Result error) {
     LOGD("%s() - entering >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", __func__);
-    oboeStream->requestStop();
-    if (oboeStream->getCallback() != nullptr) {
-        oboeStream->getCallback()->onErrorBeforeClose(oboeStream, error);
-    }
-    oboeStream->close();
-    if (oboeStream->getCallback() != nullptr) {
+    AudioStreamErrorCallback *errorCallback = oboeStream->getErrorCallback();
+    if (errorCallback == nullptr) return; // should be impossible
+    bool handled = errorCallback->onError(oboeStream, error);
+
+    if (!handled) {
+        oboeStream->requestStop();
+        errorCallback->onErrorBeforeClose(oboeStream, error);
+        oboeStream->close();
         // Warning, oboeStream may get deleted by this callback.
-        oboeStream->getCallback()->onErrorAfterClose(oboeStream, error);
+        errorCallback->onErrorAfterClose(oboeStream, error);
     }
     LOGD("%s() - exiting <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", __func__);
 }
@@ -101,14 +103,18 @@
     return openResult == 0;
 }
 
-// Static 'C' wrapper for the error callback method.
+// Static method for the error callback.
+// We use a method so we can access protected methods on the stream.
 // Launch a thread to handle the error.
 // That other thread can safely stop, close and delete the stream.
 void AudioStreamAAudio::internalErrorCallback(
         AAudioStream *stream,
         void *userData,
         aaudio_result_t error) {
+    oboe::Result oboeResult = static_cast<Result>(error);
     AudioStreamAAudio *oboeStream = reinterpret_cast<AudioStreamAAudio*>(userData);
+    LOGI("%s() oboeResult = %d", __func__, oboeResult);
+    oboeStream->mErrorCallbackResult = oboeResult;
 
     // Prevents deletion of the stream if the app is using AudioStreamBuilder::openStream(shared_ptr)
     std::shared_ptr<AudioStream> sharedStream = oboeStream->lockWeakThis();
@@ -118,16 +124,14 @@
     if (oboeStream->wasErrorCallbackCalled()) { // block extra error callbacks
         LOGE("%s() multiple error callbacks called!", __func__);
     } else if (stream != oboeStream->getUnderlyingStream()) {
-        LOGW("%s() stream already closed or closing", __func__); // can happen if there are bugs
+        LOGW("%s() stream already closed or closing", __func__); // might happen if there are bugs
     } else if (sharedStream) {
         // Handle error on a separate thread using shared pointer.
-        std::thread t(oboe_aaudio_error_thread_proc_shared, sharedStream,
-                      static_cast<Result>(error));
+        std::thread t(oboe_aaudio_error_thread_proc_shared, sharedStream, oboeResult);
         t.detach();
     } else {
         // Handle error on a separate thread.
-        std::thread t(oboe_aaudio_error_thread_proc, oboeStream,
-                      static_cast<Result>(error));
+        std::thread t(oboe_aaudio_error_thread_proc, oboeStream, oboeResult);
         t.detach();
     }
 }
@@ -229,13 +233,19 @@
 
     // TODO get more parameters from the builder?
 
-    if (mStreamCallback != nullptr) {
+    if (isDataCallbackSpecified()) {
         mLibLoader->builder_setDataCallback(aaudioBuilder, oboe_aaudio_data_callback_proc, this);
         mLibLoader->builder_setFramesPerDataCallback(aaudioBuilder, getFramesPerCallback());
-        // If the data callback is not being used then the write method will return an error
-        // and the app can stop and close the stream.
+
+        if (!isErrorCallbackSpecified()) {
+            // The app did not specify a callback so we should specify
+            // our own so the stream gets closed and stopped.
+            mErrorCallback = &mDefaultErrorCallback;
+        }
         mLibLoader->builder_setErrorCallback(aaudioBuilder, internalErrorCallback, this);
     }
+    // Else if the data callback is not being used then the write method will return an error
+    // and the app can stop and close the stream.
 
     // ============= OPEN THE STREAM ================
     {
@@ -357,7 +367,7 @@
                 return Result::OK;
             }
         }
-        if (mStreamCallback != nullptr) { // Was a callback requested?
+        if (isDataCallbackSpecified()) {
             setDataCallbackEnabled(true);
         }
         return static_cast<Result>(mLibLoader->stream_requestStart(stream));
diff --git a/src/aaudio/AudioStreamAAudio.h b/src/aaudio/AudioStreamAAudio.h
index 9095817..83f4b2f 100644
--- a/src/aaudio/AudioStreamAAudio.h
+++ b/src/aaudio/AudioStreamAAudio.h
@@ -118,10 +118,13 @@
 
     std::atomic<bool>    mCallbackThreadEnabled;
 
-    // pointer to the underlying AAudio stream, valid if open, null if closed
+    // pointer to the underlying 'C' AAudio stream, valid if open, null if closed
     std::atomic<AAudioStream *> mAAudioStream{nullptr};
 
     static AAudioLoader *mLibLoader;
+
+    // We may not use this but it is so small that it is not worth allocating dynamically.
+    AudioStreamErrorCallback mDefaultErrorCallback;
 };
 
 } // namespace oboe
diff --git a/src/common/AudioSourceCaller.cpp b/src/common/AudioSourceCaller.cpp
index 0180c22..854ade9 100644
--- a/src/common/AudioSourceCaller.cpp
+++ b/src/common/AudioSourceCaller.cpp
@@ -20,7 +20,7 @@
 using namespace flowgraph;
 
 int32_t AudioSourceCaller::onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) {
-    oboe::AudioStreamCallback *callback = mStream->getCallback();
+    oboe::AudioStreamDataCallback *callback = mStream->getDataCallback();
     int32_t result = 0;
     int32_t numFrames = numBytes / mStream->getBytesPerFrame();
     if (callback != nullptr) {
diff --git a/src/common/AudioStream.cpp b/src/common/AudioStream.cpp
index 7bcd087..8a9cd45 100644
--- a/src/common/AudioStream.cpp
+++ b/src/common/AudioStream.cpp
@@ -59,10 +59,10 @@
     }
 
     DataCallbackResult result;
-    if (mStreamCallback == nullptr) {
-        result = onDefaultCallback(audioData, numFrames);
+    if (mDataCallback) {
+        result = mDataCallback->onAudioReady(this, audioData, numFrames);
     } else {
-        result = mStreamCallback->onAudioReady(this, audioData, numFrames);
+        result = onDefaultCallback(audioData, numFrames);
     }
     // On Oreo, we might get called after returning stop.
     // So block that here.
diff --git a/src/common/DataConversionFlowGraph.cpp b/src/common/DataConversionFlowGraph.cpp
index 7cc742e..ef99d6a 100644
--- a/src/common/DataConversionFlowGraph.cpp
+++ b/src/common/DataConversionFlowGraph.cpp
@@ -101,8 +101,9 @@
     // Source
     // IF OUTPUT and using a callback then call back to the app using a SourceCaller.
     // OR IF INPUT and NOT using a callback then read from the child stream using a SourceCaller.
-    if ((sourceStream->getCallback() != nullptr && isOutput)
-        || (sourceStream->getCallback() == nullptr && isInput)) {
+    bool isDataCallbackSpecified = sourceStream->isDataCallbackSpecified();
+    if ((isDataCallbackSpecified && isOutput)
+        || (!isDataCallbackSpecified && isInput)) {
         int32_t actualSourceFramesPerCallback = (sourceFramesPerCallback == kUnspecified)
                 ? sourceStream->getFramesPerBurst()
                 : sourceFramesPerCallback;
@@ -236,7 +237,7 @@
 
 int32_t DataConversionFlowGraph::onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) {
     int32_t numFrames = numBytes / mFilterStream->getBytesPerFrame();
-    mCallbackResult = mFilterStream->getCallback()->onAudioReady(mFilterStream, buffer, numFrames);
+    mCallbackResult = mFilterStream->getDataCallback()->onAudioReady(mFilterStream, buffer, numFrames);
     // TODO handle STOP from callback, process data remaining in the block adapter
     return numBytes;
-}
\ No newline at end of file
+}
diff --git a/src/common/FilterAudioStream.cpp b/src/common/FilterAudioStream.cpp
index a471583..bacb54b 100644
--- a/src/common/FilterAudioStream.cpp
+++ b/src/common/FilterAudioStream.cpp
@@ -16,6 +16,7 @@
 
 #include <memory>
 
+#include "OboeDebug.h"
 #include "FilterAudioStream.h"
 
 using namespace oboe;
@@ -90,3 +91,17 @@
     return ResultWithValue<int32_t>::createBasedOnSign(framesRead);
 }
 
+DataCallbackResult FilterAudioStream::onAudioReady(AudioStream *oboeStream,
+                                void *audioData,
+                                int32_t numFrames) {
+    int32_t framesProcessed;
+    if (oboeStream->getDirection() == Direction::Output) {
+        framesProcessed = mFlowGraph->read(audioData, numFrames, 0 /* timeout */);
+    } else {
+        framesProcessed = mFlowGraph->write(audioData, numFrames);
+    }
+    LOGI("%s() framesProcessed = %d, numFrames = %d", __func__, framesProcessed, numFrames);
+    return (framesProcessed < numFrames)
+           ? DataCallbackResult::Stop
+           : mFlowGraph->getDataCallbackResult();
+}
\ No newline at end of file
diff --git a/src/common/FilterAudioStream.h b/src/common/FilterAudioStream.h
index 3949de7..be652c9 100644
--- a/src/common/FilterAudioStream.h
+++ b/src/common/FilterAudioStream.h
@@ -42,8 +42,11 @@
     : AudioStream(builder)
     , mChildStream(childStream) {
         // Intercept the callback if used.
-        if (builder.getCallback() != nullptr) {
-            mStreamCallback = mChildStream->swapCallback(this);
+        if (builder.getErrorCallback() != nullptr) {
+            mErrorCallback = mChildStream->swapErrorCallback(this);
+        }
+        if (builder.getDataCallback() != nullptr) {
+            mDataCallback = mChildStream->swapDataCallback(this);
         } else {
             const int size = childStream->getFramesPerBurst() * childStream->getBytesPerFrame();
             mBlockingBuffer = std::make_unique<uint8_t[]>(size);
@@ -175,32 +178,36 @@
 
     DataCallbackResult onAudioReady(AudioStream *oboeStream,
             void *audioData,
-            int32_t numFrames) override {
-        int32_t framesProcessed;
-        if (oboeStream->getDirection() == Direction::Output) {
-            framesProcessed = mFlowGraph->read(audioData, numFrames, 0 /* timeout */);
-        } else {
-            framesProcessed = mFlowGraph->write(audioData, numFrames);
+            int32_t numFrames) override;
+
+    bool onError(AudioStream * audioStream, Result error) override {
+        if (mErrorCallback != nullptr) {
+            return mErrorCallback->onError(this, error);
         }
-        return (framesProcessed < numFrames)
-                ? DataCallbackResult::Stop
-                : mFlowGraph->getDataCallbackResult();
+        return false;
     }
 
     void onErrorBeforeClose(AudioStream *oboeStream, Result error) override {
-        if (mStreamCallback != nullptr) {
-            mStreamCallback->onErrorBeforeClose(this, error);
+        if (mErrorCallback != nullptr) {
+            mErrorCallback->onErrorBeforeClose(this, error);
         }
     }
 
     void onErrorAfterClose(AudioStream *oboeStream, Result error) override {
         // Close this parent stream because the callback will only close the child.
         AudioStream::close();
-        if (mStreamCallback != nullptr) {
-            mStreamCallback->onErrorAfterClose(this, error);
+        if (mErrorCallback != nullptr) {
+            mErrorCallback->onErrorAfterClose(this, error);
         }
     }
 
+    /**
+     * @return last result passed from an error callback
+     */
+    oboe::Result getLastErrorCallbackResult() const override {
+        return mChildStream->getLastErrorCallbackResult();
+    }
+
 private:
 
     std::unique_ptr<AudioStream>             mChildStream; // this stream wraps the child stream
diff --git a/src/common/QuirksManager.cpp b/src/common/QuirksManager.cpp
index 21a5204..68f328e 100644
--- a/src/common/QuirksManager.cpp
+++ b/src/common/QuirksManager.cpp
@@ -138,7 +138,7 @@
     // know if we will get an MMAP stream. So, to be safe, just do the conversion in Oboe.
     if (OboeGlobals::areWorkaroundsEnabled()
             && builder.willUseAAudio()
-            && builder.getCallback() != nullptr
+            && builder.isDataCallbackSpecified()
             && builder.getFramesPerCallback() != 0
             && getSdkVersion() <= __ANDROID_API_R__) {
         LOGI("QuirksManager::%s() avoid setFramesPerCallback(n>0)", __func__);
diff --git a/src/opensles/AudioStreamBuffered.cpp b/src/opensles/AudioStreamBuffered.cpp
index 85b9f8e..8ee0166 100644
--- a/src/opensles/AudioStreamBuffered.cpp
+++ b/src/opensles/AudioStreamBuffered.cpp
@@ -260,7 +260,7 @@
 
 bool AudioStreamBuffered::isXRunCountSupported() const {
     // XRun count is only supported if we're using blocking I/O (not callbacks)
-    return (getCallback() == nullptr);
+    return (!isDataCallbackSpecified());
 }
 
 } // namespace oboe
\ No newline at end of file
diff --git a/src/opensles/AudioStreamBuffered.h b/src/opensles/AudioStreamBuffered.h
index 5923e8d..2b6152b 100644
--- a/src/opensles/AudioStreamBuffered.h
+++ b/src/opensles/AudioStreamBuffered.h
@@ -60,7 +60,7 @@
     DataCallbackResult onDefaultCallback(void *audioData, int numFrames) override;
 
     // If there is no callback then we need a FIFO between the App and OpenSL ES.
-    bool usingFIFO() const { return getCallback() == nullptr; }
+    bool usingFIFO() const { return !isDataCallbackSpecified(); }
 
     virtual Result updateServiceFrameCounter() = 0;
 
diff --git a/tests/testStreamClosedMethods.cpp b/tests/testStreamClosedMethods.cpp
index a1f1105..cbb2191 100644
--- a/tests/testStreamClosedMethods.cpp
+++ b/tests/testStreamClosedMethods.cpp
@@ -19,15 +19,13 @@
 
 using namespace oboe;
 
-class MyCallback : public AudioStreamCallback {
+class MyCallback : public AudioStreamDataCallback {
 public:
     DataCallbackResult onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override {
         return DataCallbackResult::Continue;
     }
 };
 
-
-
 class StreamClosedReturnValues : public ::testing::Test {
 
 protected:
@@ -134,13 +132,13 @@
     ASSERT_EQ(mStream->getDeviceId(), d);
 }
 
-TEST_F(StreamClosedReturnValues, GetCallbackReturnsLastKnownValue) {
+TEST_F(StreamClosedReturnValues, GetDataCallbackReturnsLastKnownValue) {
 
-    AudioStreamCallback *callback = new MyCallback();
-    mBuilder.setCallback(callback);
+    AudioStreamDataCallback *callback = new MyCallback();
+    mBuilder.setDataCallback(callback);
     ASSERT_TRUE(openAndCloseStream());
 
-    AudioStreamCallback *callback2 = mStream->getCallback();
+    AudioStreamDataCallback *callback2 = mStream->getDataCallback();
     ASSERT_EQ(callback, callback2);
 }