OboeTester: major refactoring of NativeAudioContext

Use a different class for each test activity.
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h
index d892016..98cf5b7 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h
@@ -43,6 +43,7 @@
     }
 
     virtual oboe::Result start();
+
     virtual oboe::Result stop();
 
     /**
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index db563b6..d113028 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -30,12 +30,50 @@
     }
 }
 
-NativeAudioContext::NativeAudioContext()
-    : sineOscillators(MAX_SINE_OSCILLATORS)
-    , sawtoothOscillators(MAX_SINE_OSCILLATORS) {
+bool ActivityContext::useCallback = true;
+bool ActivityContext::callbackReturnStop = false;
+int  ActivityContext::callbackSize = 0;
+
+oboe::AudioStream * ActivityContext::getOutputStream() {
+    for (int32_t i = 0; i < kMaxStreams; i++) {
+        oboe::AudioStream *oboeStream = mOboeStreams[i];
+        if (oboeStream != nullptr) {
+            if (oboeStream->getDirection() == oboe::Direction::Output) {
+                return oboeStream;
+            }
+        }
+    }
+    return nullptr;
 }
 
-void NativeAudioContext::close(int32_t streamIndex) {
+oboe::AudioStream * ActivityContext::getInputStream() {
+    for (int32_t i = 0; i < kMaxStreams; i++) {
+        oboe::AudioStream *oboeStream = mOboeStreams[i];
+        if (oboeStream != nullptr) {
+            if (oboeStream->getDirection() == oboe::Direction::Input) {
+                return oboeStream;
+            }
+        }
+    }
+    return nullptr;
+}
+
+void ActivityContext::freeStreamIndex(int32_t streamIndex) {
+    mOboeStreams[streamIndex] = nullptr;
+}
+
+int32_t ActivityContext::allocateStreamIndex() {
+    int32_t streamIndex = -1;
+    for (int32_t i = 0; i < kMaxStreams; i++) {
+        if (mOboeStreams[i] == nullptr) {
+            streamIndex = i;
+            break;
+        }
+    }
+    return streamIndex;
+}
+
+void ActivityContext::close(int32_t streamIndex) {
     stopBlockingIOThread();
 
     LOGD("%s() delete stream %d ", __func__, streamIndex);
@@ -44,15 +82,9 @@
         delete mOboeStreams[streamIndex];
         freeStreamIndex(streamIndex);
     }
-
-    LOGD("%s() delete nodes", __func__);
-    manyToMulti.reset(nullptr);
-    monoToMulti.reset(nullptr);
-    mSinkFloat.reset();
-    mSinkI16.reset();
 }
 
-bool NativeAudioContext::isMMapUsed(int32_t streamIndex) {
+bool ActivityContext::isMMapUsed(int32_t streamIndex) {
     oboe::AudioStream *oboeStream = getStream(streamIndex);
     if (oboeStream != nullptr && oboeStream->usesAAudio()) {
         if (mAAudioStream_isMMap == nullptr) {
@@ -76,39 +108,162 @@
     return false;
 }
 
-void NativeAudioContext::connectTone() {
-//    if (monoToMulti != nullptr) {
-//        LOGI("%s() mToneType = %d", __func__, mToneType);
-//        switch (mToneType) {
-//            case ToneType::SawPing:
-//                sawPingGenerator.output.connect(&(monoToMulti->input));
-//                monoToMulti->output.connect(&(mSinkFloat.get()->input));
-//                monoToMulti->output.connect(&(mSinkI16.get()->input));
-//                break;
-//            case ToneType::Sine:
-//                for (int i = 0; i < mChannelCount; i++) {
-//                    sineOscillators[i].output.connect(manyToMulti->inputs[i].get());
-//                }
-//                manyToMulti->output.connect(&(mSinkFloat.get()->input));
-//                manyToMulti->output.connect(&(mSinkI16.get()->input));
-//                break;
-//            case ToneType::Impulse:
-//                impulseGenerator.output.connect(&(monoToMulti->input));
-//                monoToMulti->output.connect(&(mSinkFloat.get()->input));
-//                monoToMulti->output.connect(&(mSinkI16.get()->input));
-//                break;
-//            case ToneType::Sawtooth:
-//                for (int i = 0; i < mChannelCount; i++) {
-//                    sawtoothOscillators[i].output.connect(manyToMulti->inputs[i].get());
-//                }
-//                manyToMulti->output.connect(&(mSinkFloat.get()->input));
-//                manyToMulti->output.connect(&(mSinkI16.get()->input));
-//                break;
-//        }
-//    }
+oboe::Result ActivityContext::pause() {
+    LOGD("NativeAudioContext::%s() called", __func__);
+    oboe::Result result = oboe::Result::OK;
+    stopBlockingIOThread();
+    for (int32_t i = 0; i < kMaxStreams; i++) {
+        oboe::AudioStream *oboeStream = mOboeStreams[i];
+        if (oboeStream != nullptr) {
+            result = oboeStream->requestPause();
+            printScheduler();
+        }
+    }
+    return result;
 }
 
-void NativeAudioContext::setChannelEnabled(int channelIndex, bool enabled) {
+oboe::Result ActivityContext::stopAllStreams() {
+    oboe::Result result = oboe::Result::OK;
+    stopBlockingIOThread();
+
+    for (int32_t i = 0; i < kMaxStreams; i++) {
+        oboe::AudioStream *oboeStream = mOboeStreams[i];
+        if (oboeStream != nullptr) {
+            result = oboeStream->requestStop();
+            printScheduler();
+        }
+    }
+    return result;
+}
+
+
+void ActivityContext::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+    // We needed the proxy because we did not know the channelCount when we setup the Builder.
+    if (useCallback) {
+        LOGD("ActivityContext::open() set callback to use oboeCallbackProxy, size = %d",
+             callbackSize);
+        builder.setCallback(&oboeCallbackProxy);
+        builder.setFramesPerCallback(callbackSize);
+    }
+}
+
+int ActivityContext::open(
+        jint nativeApi,
+        jint sampleRate,
+        jint channelCount,
+        jint format,
+        jint sharingMode,
+        jint performanceMode,
+        jint deviceId,
+        jint sessionId,
+        jint framesPerBurst,
+        jboolean isInput) {
+
+    oboe::AudioApi audioApi = oboe::AudioApi::Unspecified;
+    switch (nativeApi) {
+        case NATIVE_MODE_UNSPECIFIED:
+        case NATIVE_MODE_AAUDIO:
+        case NATIVE_MODE_OPENSLES:
+            audioApi = convertNativeApiToAudioApi(nativeApi);
+            break;
+        default:
+            return (jint) oboe::Result::ErrorOutOfRange;
+    }
+
+    int32_t streamIndex = allocateStreamIndex();
+    if (streamIndex < 0) {
+        LOGE("ActivityContext::open() stream array full");
+        return (jint) oboe::Result::ErrorNoFreeHandles;
+    }
+
+    if (channelCount < 0 || channelCount > 256) {
+        LOGE("ActivityContext::open() channels out of range");
+        return (jint) oboe::Result::ErrorOutOfRange;
+    }
+
+    // Create an audio output stream.
+    LOGD("ActivityContext::open() try to create OboeStream #%d", streamIndex);
+    oboe::AudioStreamBuilder builder;
+    builder.setChannelCount(channelCount)
+            ->setDirection(isInput ? oboe::Direction::Input : oboe::Direction::Output)
+            ->setSharingMode((oboe::SharingMode) sharingMode)
+            ->setPerformanceMode((oboe::PerformanceMode) performanceMode)
+            ->setDeviceId(deviceId)
+            ->setSessionId((oboe::SessionId) sessionId)
+            ->setSampleRate(sampleRate)
+            ->setFormat((oboe::AudioFormat) format);
+
+    configureBuilder(isInput, builder);
+
+    if (audioApi == oboe::AudioApi::OpenSLES) {
+        builder.setFramesPerCallback(framesPerBurst);
+    }
+    builder.setAudioApi(audioApi);
+
+    // Open a stream based on the builder settings.
+    oboe::AudioStream *oboeStream = nullptr;
+    oboe::Result result = builder.openStream(&oboeStream);
+    LOGD("ActivityContext::open() builder.openStream() returned %d", result);
+    if (result != oboe::Result::OK) {
+        delete oboeStream;
+        oboeStream = nullptr;
+        freeStreamIndex(streamIndex);
+        streamIndex = -1;
+    } else {
+        mOboeStreams[streamIndex] = oboeStream;
+
+        mChannelCount = oboeStream->getChannelCount(); // FIXME store per stream
+        mFramesPerBurst = oboeStream->getFramesPerBurst();
+        mSampleRate = oboeStream->getSampleRate();
+
+        finishOpen(isInput, oboeStream);
+    }
+
+    if (!useCallback) {
+        int numSamples = getFramesPerBlock() * mChannelCount;
+        dataBuffer = std::make_unique<float[]>(numSamples);
+    }
+
+    return ((int)result < 0) ? (int)result : streamIndex;
+}
+
+oboe::Result ActivityContext::start() {
+    LOGD("ActivityContext: %s() called", __func__);
+    oboe::Result result = oboe::Result::OK;
+    oboe::AudioStream *inputStream = getInputStream();
+    oboe::AudioStream *outputStream = getOutputStream();
+    if (inputStream == nullptr && outputStream == nullptr) {
+        LOGD("%s() - no streams defined", __func__);
+        return oboe::Result::ErrorInvalidState; // not open
+    }
+
+    stop();
+
+    configureForStart();
+
+    result = startStreams();
+
+    if (!useCallback && result == oboe::Result::OK) {
+        LOGD("start thread for blocking I/O");
+        // Instead of using the callback, start a thread that writes the stream.
+        threadEnabled.store(true);
+        dataThread = new std::thread(threadCallback, this);
+    }
+
+    return result;
+}
+
+// =================================================================== ActivityTestOutput
+void ActivityTestOutput::close(int32_t streamIndex) {
+    ActivityContext::close(streamIndex);
+    manyToMulti.reset(nullptr);
+    monoToMulti.reset(nullptr);
+    mSinkFloat.reset();
+    mSinkI16.reset();
+}
+
+
+void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
     if (manyToMulti == nullptr) {
         return;
     }
@@ -128,292 +283,59 @@
     }
 }
 
-int32_t NativeAudioContext::allocateStreamIndex() {
-    int32_t streamIndex = -1;
-    for (int32_t i = 0; i < kMaxStreams; i++) {
-        if (mOboeStreams[i] == nullptr) {
-            streamIndex = i;
-            break;
-        }
-    }
-    return streamIndex;
-}
-
-void NativeAudioContext::freeStreamIndex(int32_t streamIndex) {
-    mOboeStreams[streamIndex] = nullptr;
-}
-
-int NativeAudioContext::open(
-        jint nativeApi,
-        jint sampleRate,
-        jint channelCount,
-        jint format,
-        jint sharingMode,
-        jint performanceMode,
-        jint deviceId,
-        jint sessionId,
-        jint framesPerBurst, jboolean isInput) {
-
-    oboe::AudioApi audioApi = oboe::AudioApi::Unspecified;
-    switch (nativeApi) {
-        case NATIVE_MODE_UNSPECIFIED:
-        case NATIVE_MODE_AAUDIO:
-        case NATIVE_MODE_OPENSLES:
-            audioApi = convertNativeApiToAudioApi(nativeApi);
-            break;
-        default:
-            return (jint) oboe::Result::ErrorOutOfRange;
-    }
-
-    int32_t streamIndex = allocateStreamIndex();
-    if (streamIndex < 0) {
-        LOGE("NativeAudioContext::open() stream array full");
-        return (jint) oboe::Result::ErrorNoFreeHandles;
-    }
-
-    if (channelCount < 0 || channelCount > 256) {
-        LOGE("NativeAudioContext::open() channels out of range");
-        return (jint) oboe::Result::ErrorOutOfRange;
-    }
-
-    // Create an audio output stream.
-    LOGD("NativeAudioContext::open() try to create OboeStream #%d", streamIndex);
-    oboe::AudioStreamBuilder builder;
-    builder.setChannelCount(channelCount)
-            ->setDirection(isInput ? oboe::Direction::Input : oboe::Direction::Output)
-            ->setSharingMode((oboe::SharingMode) sharingMode)
-            ->setPerformanceMode((oboe::PerformanceMode) performanceMode)
-            ->setDeviceId(deviceId)
-            ->setSessionId((oboe::SessionId) sessionId)
-            ->setSampleRate(sampleRate)
-            ->setFormat((oboe::AudioFormat) format);
-
-    if (mActivityType == ActivityType::Echo) {
-        if (mFullDuplexEcho.get() == nullptr) {
-            mFullDuplexEcho = std::make_unique<FullDuplexEcho>();
-        }
-        // only output uses a callback, input is polled
-        if (!isInput) {
-            builder.setCallback(mFullDuplexEcho.get());
-        }
-    } else {
-        // We needed the proxy because we did not know the channelCount when we setup the Builder.
-        if (useCallback) {
-            LOGD("NativeAudioContext::open() set callback to use oboeCallbackProxy, size = %d",
-                 callbackSize);
-            builder.setCallback(&oboeCallbackProxy);
-            builder.setFramesPerCallback(callbackSize);
-        }
-    }
-
-    if (audioApi == oboe::AudioApi::OpenSLES) {
-        builder.setFramesPerCallback(framesPerBurst);
-    }
-    builder.setAudioApi(audioApi);
-
-    // Open a stream based on the builder settings.
-    oboe::AudioStream *oboeStream = nullptr;
-    oboe::Result result = builder.openStream(&oboeStream);
-    LOGD("NativeAudioContext::open() open(b) returned %d", result);
-    if (result != oboe::Result::OK) {
-        delete oboeStream;
-        oboeStream = nullptr;
-        freeStreamIndex(streamIndex);
-        streamIndex = -1;
-    } else {
-        mOboeStreams[streamIndex] = oboeStream;
-
-        mChannelCount = oboeStream->getChannelCount(); // FIXME store per stream
-        mFramesPerBurst = oboeStream->getFramesPerBurst();
-        mSampleRate = oboeStream->getSampleRate();
-
-        if (mActivityType == ActivityType::Echo) {
-            if (isInput) {
-                mFullDuplexEcho->setInputStream(oboeStream);
-            } else {
-                mFullDuplexEcho->setOutputStream(oboeStream);
-            }
-        }
-    }
-
-    return ((int)result < 0) ? (int)result : streamIndex;
-}
-
-oboe::AudioStream * NativeAudioContext::getOutputStream() {
-    for (int32_t i = 0; i < kMaxStreams; i++) {
-        oboe::AudioStream *oboeStream = mOboeStreams[i];
-        if (oboeStream != nullptr) {
-            if (oboeStream->getDirection() == oboe::Direction::Output) {
-                return oboeStream;
-            }
-        }
-    }
-    return nullptr;
-}
-
-oboe::AudioStream * NativeAudioContext::getInputStream() {
-    for (int32_t i = 0; i < kMaxStreams; i++) {
-        oboe::AudioStream *oboeStream = mOboeStreams[i];
-        if (oboeStream != nullptr) {
-            if (oboeStream->getDirection() == oboe::Direction::Input) {
-                return oboeStream;
-            }
-        }
-    }
-    return nullptr;
-}
-
-void NativeAudioContext::configureForActivityType() {
-    oboe::AudioStream *outputStream = nullptr;
-
+void ActivityTestOutput::configureForStart() {
     manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
-    monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
 
     mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
     mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
 
-    // TODO Use ActivityContext classes instead of switches.
-    switch(mActivityType) {
-        case ActivityType::Undefined:
-            break;
-        case ActivityType::TestInput:
-        case ActivityType::RecordPlay:
-            mInputAnalyzer.reset();
-            if (useCallback) {
-                oboeCallbackProxy.setCallback(&mInputAnalyzer);
-            }
-            mRecording = std::make_unique<MultiChannelRecording>(mChannelCount,
-                                                                 SECONDS_TO_RECORD * mSampleRate);
-            mInputAnalyzer.setRecording(mRecording.get());
-            break;
-
-        case ActivityType::TestOutput:
-            outputStream = getOutputStream();
-            {
-                double frequency = 440.0;
-                for (int i = 0; i < mChannelCount; i++) {
-                    sineOscillators[i].setSampleRate(outputStream->getSampleRate());
-                    sineOscillators[i].frequency.setValue(frequency);
-                    frequency *= 4.0 / 3.0; // each sine is at a higher frequency
-                    sineOscillators[i].amplitude.setValue(AMPLITUDE_SINE);
-                    sineOscillators[i].output.connect(manyToMulti->inputs[i].get());
-                }
-            }
-
-            manyToMulti->output.connect(&(mSinkFloat.get()->input));
-            manyToMulti->output.connect(&(mSinkI16.get()->input));
-            break;
-
-        case ActivityType::TapToTone:
-            outputStream = getOutputStream();
-            sawPingGenerator.setSampleRate(outputStream->getSampleRate());
-            sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
-            sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
-
-            sawPingGenerator.output.connect(&(monoToMulti->input));
-            monoToMulti->output.connect(&(mSinkFloat.get()->input));
-            monoToMulti->output.connect(&(mSinkI16.get()->input));
-
-            sawPingGenerator.setEnabled(false);
-            break;
-
-        case ActivityType::Echo:
-            break;
-    }
-
-    if (outputStream != nullptr) {
-
-        mSinkFloat->start();
-        mSinkI16->start();
-
-        // We needed the proxy because we did not know the channelCount
-        // when we setup the Builder.
-        if (outputStream->getFormat() == oboe::AudioFormat::I16) {
-            audioStreamGateway.setAudioSink(mSinkI16);
-        } else if (outputStream->getFormat() == oboe::AudioFormat::Float) {
-            audioStreamGateway.setAudioSink(mSinkFloat);
-        }
-
-        if (useCallback) {
-            oboeCallbackProxy.setCallback(&audioStreamGateway);
-        }
-
-        // Set starting size of buffer.
-        constexpr int kDefaultNumBursts = 2; // "double buffer"
-        int32_t numBursts = kDefaultNumBursts;
-        // callbackSize is used for both callbacks and blocking write
-        numBursts = (callbackSize <= mFramesPerBurst)
-                    ? kDefaultNumBursts
-                    : ((callbackSize * kDefaultNumBursts) + mFramesPerBurst - 1)
-                      / mFramesPerBurst;
-        outputStream->setBufferSizeInFrames(numBursts * mFramesPerBurst);
-    }
-
-    if (!useCallback) {
-        int numSamples = getFramesPerBlock() * mChannelCount;
-        dataBuffer = std::make_unique<float[]>(numSamples);
-    }
-}
-
-oboe::Result NativeAudioContext::start() {
-    oboe::Result result = oboe::Result::OK;
-    oboe::AudioStream *inputStream = getInputStream();
     oboe::AudioStream *outputStream = getOutputStream();
-    if (inputStream == nullptr && outputStream == nullptr) {
-        return oboe::Result::ErrorInvalidState; // not open
+    {
+        double frequency = 440.0;
+        for (int i = 0; i < mChannelCount; i++) {
+            sineOscillators[i].setSampleRate(outputStream->getSampleRate());
+            sineOscillators[i].frequency.setValue(frequency);
+            frequency *= 4.0 / 3.0; // each sine is at a higher frequency
+            sineOscillators[i].amplitude.setValue(AMPLITUDE_SINE);
+            sineOscillators[i].output.connect(manyToMulti->inputs[i].get());
+        }
     }
 
-    stop();
+    manyToMulti->output.connect(&(mSinkFloat.get()->input));
+    manyToMulti->output.connect(&(mSinkI16.get()->input));
 
-    LOGD("NativeAudioContext: %s() called", __func__);
-    configureForActivityType();
-
-    switch(mActivityType) {
-        case ActivityType::Undefined:
-            break;
-        case ActivityType::TestInput:
-        case ActivityType::RecordPlay:
-            result = inputStream->requestStart();
-            if (!useCallback && result == oboe::Result::OK) {
-                LOGD("start thread for blocking I/O");
-                // Instead of using the callback, start a thread that readsthe stream.
-                threadEnabled.store(true);
-                dataThread = new std::thread(threadCallback, this);
-            }
-            break;
-
-        case ActivityType::TestOutput:
-        case ActivityType::TapToTone:
-            result = outputStream->requestStart();
-            if (!useCallback && result == oboe::Result::OK) {
-                LOGD("start thread for blocking I/O");
-                // Instead of using the callback, start a thread that writes the stream.
-                threadEnabled.store(true);
-                dataThread = new std::thread(threadCallback, this);
-            }
-            break;
-
-        case ActivityType::Echo:
-            result = mFullDuplexEcho->start();
-            break;
-    }
-
-    LOGD("OboeAudioStream_start: start returning %d", result);
-    return result;
+    configureStreamGateway();
 }
 
-void NativeAudioContext::runBlockingIO() {
+void ActivityTestOutput::configureStreamGateway() {
+    oboe::AudioStream *outputStream = getOutputStream();
+    if (outputStream->getFormat() == oboe::AudioFormat::I16) {
+        audioStreamGateway.setAudioSink(mSinkI16);
+    } else if (outputStream->getFormat() == oboe::AudioFormat::Float) {
+        audioStreamGateway.setAudioSink(mSinkFloat);
+    }
+
+    if (useCallback) {
+        oboeCallbackProxy.setCallback(&audioStreamGateway);
+    }
+
+    // Set starting size of buffer.
+    constexpr int kDefaultNumBursts = 2; // "double buffer"
+    int32_t numBursts = kDefaultNumBursts;
+    // callbackSize is used for both callbacks and blocking write
+    numBursts = (callbackSize <= mFramesPerBurst)
+                ? kDefaultNumBursts
+                : ((callbackSize * kDefaultNumBursts) + mFramesPerBurst - 1)
+                  / mFramesPerBurst;
+    outputStream->setBufferSizeInFrames(numBursts * mFramesPerBurst);
+
+}
+
+void ActivityTestOutput::runBlockingIO() {
     int32_t framesPerBlock = getFramesPerBlock();
     oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
 
-    // TODO rethink which stream gets the callback for full duplex
-    oboe::AudioStream *oboeStream = nullptr;
-    for (int32_t i = 0; i < kMaxStreams; i++) {
-        if (mOboeStreams[i] != nullptr) {
-            oboeStream = mOboeStreams[i];
-            break;
-        }
-    }
+    oboe::AudioStream *oboeStream = getOutputStream();
     if (oboeStream == nullptr) {
         LOGE("%s() : no stream found\n", __func__);
         return;
@@ -421,45 +343,144 @@
 
     while (threadEnabled.load()
            && callbackResult == oboe::DataCallbackResult::Continue) {
-        if (oboeStream->getDirection() == oboe::Direction::Input) {
-            // read from input
-            auto result = oboeStream->read(dataBuffer.get(),
-                                           framesPerBlock,
-                                           NANOS_PER_SECOND);
-            if (!result) {
-                LOGE("%s() : read() returned %s\n", __func__, convertToText(result.error()));
-                break;
-            }
-            int32_t framesRead = result.value();
-            if (framesRead < framesPerBlock) { // timeout?
-                LOGE("%s() : read() read %d of %d\n", __func__, framesRead, framesPerBlock);
-                break;
-            }
-
-            // analyze input
-            callbackResult = mInputAnalyzer.onAudioReady(oboeStream,
+        // generate output by calling the callback
+        callbackResult = audioStreamGateway.onAudioReady(oboeStream,
                                                          dataBuffer.get(),
-                                                         framesRead);
-        } else {  // OUTPUT?
-            // generate output by calling the callback
-            callbackResult = audioStreamGateway.onAudioReady(oboeStream,
-                                                              dataBuffer.get(),
-                                                              framesPerBlock);
+                                                         framesPerBlock);
 
-            auto result = oboeStream->write(dataBuffer.get(),
-                                            framesPerBlock,
-                                            NANOS_PER_SECOND);
+        auto result = oboeStream->write(dataBuffer.get(),
+                                        framesPerBlock,
+                                        NANOS_PER_SECOND);
 
-            if (!result) {
-                LOGE("%s() returned %s\n", __func__, convertToText(result.error()));
-                break;
-            }
-            int32_t framesWritten = result.value();
-            if (framesWritten < framesPerBlock) {
-                LOGE("%s() : write() wrote %d of %d\n", __func__, framesWritten, framesPerBlock);
-                break;
-            }
+        if (!result) {
+            LOGE("%s() returned %s\n", __func__, convertToText(result.error()));
+            break;
+        }
+        int32_t framesWritten = result.value();
+        if (framesWritten < framesPerBlock) {
+            LOGE("%s() : write() wrote %d of %d\n", __func__, framesWritten, framesPerBlock);
+            break;
         }
     }
+}
 
+// ======================================================================= ActivityTestInput
+void ActivityTestInput::configureForStart() {
+    mInputAnalyzer.reset();
+    if (useCallback) {
+        oboeCallbackProxy.setCallback(&mInputAnalyzer);
+    }
+    mRecording = std::make_unique<MultiChannelRecording>(mChannelCount,
+                                                         SECONDS_TO_RECORD * mSampleRate);
+    mInputAnalyzer.setRecording(mRecording.get());
+}
+
+void ActivityTestInput::runBlockingIO() {
+    int32_t framesPerBlock = getFramesPerBlock();
+    oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
+
+    oboe::AudioStream *oboeStream = getInputStream();
+    if (oboeStream == nullptr) {
+        LOGE("%s() : no stream found\n", __func__);
+        return;
+    }
+
+    while (threadEnabled.load()
+           && callbackResult == oboe::DataCallbackResult::Continue) {
+        // read from input
+        auto result = oboeStream->read(dataBuffer.get(),
+                                       framesPerBlock,
+                                       NANOS_PER_SECOND);
+        if (!result) {
+            LOGE("%s() : read() returned %s\n", __func__, convertToText(result.error()));
+            break;
+        }
+        int32_t framesRead = result.value();
+        if (framesRead < framesPerBlock) { // timeout?
+            LOGE("%s() : read() read %d of %d\n", __func__, framesRead, framesPerBlock);
+            break;
+        }
+
+        // analyze input
+        callbackResult = mInputAnalyzer.onAudioReady(oboeStream,
+                                                     dataBuffer.get(),
+                                                     framesRead);
+    }
+}
+
+oboe::Result ActivityRecording::stopPlayback() {
+    LOGD("ActivityRecording::%s() called", __func__);
+    oboe::Result result = oboe::Result::OK;
+    if (playbackStream != nullptr) {
+        result = playbackStream->requestStop();
+        playbackStream->close();
+        mPlayRecordingCallback.setRecording(nullptr);
+        delete playbackStream;
+        playbackStream = nullptr;
+    }
+    return result;
+}
+
+oboe::Result ActivityRecording::startPlayback() {
+    stop();
+    LOGD("ActivityRecording::%s() called", __func__);
+    oboe::AudioStreamBuilder builder;
+    builder.setChannelCount(mChannelCount)
+            ->setSampleRate(mSampleRate)
+            ->setFormat(oboe::AudioFormat::Float)
+            ->setCallback(&mPlayRecordingCallback)
+            ->setAudioApi(oboe::AudioApi::OpenSLES);
+    oboe::Result result = builder.openStream(&playbackStream);
+    LOGD("ActivityRecording::%s() openStream() returned %d", __func__, result);
+    if (result != oboe::Result::OK) {
+        delete playbackStream;
+        playbackStream = nullptr;
+    } else if (playbackStream != nullptr) {
+        if (mRecording != nullptr) {
+            mRecording->rewind();
+            mPlayRecordingCallback.setRecording(mRecording.get());
+            result = playbackStream->requestStart();
+        }
+    }
+    return result;
+}
+
+
+// ======================================================================= ActivityTapToTone
+void ActivityTapToTone::configureForStart() {
+    monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
+
+    mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
+    mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
+
+    oboe::AudioStream *outputStream = getOutputStream();
+    sawPingGenerator.setSampleRate(outputStream->getSampleRate());
+    sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
+    sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
+
+    sawPingGenerator.output.connect(&(monoToMulti->input));
+    monoToMulti->output.connect(&(mSinkFloat.get()->input));
+    monoToMulti->output.connect(&(mSinkI16.get()->input));
+
+    sawPingGenerator.setEnabled(false);
+    configureStreamGateway();
+}
+
+// ======================================================================= ActivityEcho
+void ActivityEcho::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+    if (mFullDuplexEcho.get() == nullptr) {
+        mFullDuplexEcho = std::make_unique<FullDuplexEcho>();
+    }
+    // only output uses a callback, input is polled
+    if (!isInput) {
+        builder.setCallback(mFullDuplexEcho.get());
+    }
+}
+
+void ActivityEcho::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
+    if (isInput) {
+        mFullDuplexEcho->setInputStream(oboeStream);
+    } else {
+        mFullDuplexEcho->setOutputStream(oboeStream);
+    }
 }
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index ec0d3e9..e0a273e 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -62,23 +62,20 @@
 #define FUNCTION_IS_MMAP         "AAudioStream_isMMapUsed"
 
 /**
- * Implement the native API for the Oboe Tester.
- * Manage a stream.
- * Generate signals, etc.
+ * Abstract base class that corresponds to a test at the Java level.
  */
-class NativeAudioContext {
+class ActivityContext {
 public:
 
-    NativeAudioContext();
-
-    void close(int32_t streamIndex);
-
-    bool isMMapUsed(int32_t streamIndex);
+    ActivityContext() {}
+    virtual ~ActivityContext() = default;
 
     oboe::AudioStream *getStream(int32_t streamIndex) {
         return mOboeStreams[streamIndex]; // TODO range check
     }
 
+    virtual void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder);
+
     int open(jint nativeApi,
              jint sampleRate,
              jint channelCount,
@@ -87,163 +84,202 @@
              jint performanceMode,
              jint deviceId,
              jint sessionId,
-             jint framesPerBurst, jboolean isInput);
+             jint framesPerBurst,
+             jboolean isInput);
 
-    void setToneType(int toneType) {
-        LOGI("%s(%d)", __func__, toneType);
-        mToneType = (ToneType) toneType;
-        // connectTone();
-    }
 
-    int32_t getFramesPerBlock() {
-        return (callbackSize == 0) ? mFramesPerBurst : callbackSize;
-    }
+    virtual void close(int32_t streamIndex);
 
     void printScheduler() {
         int scheduler = audioStreamGateway.getScheduler();
         LOGI("scheduler = 0x%08x, SCHED_FIFO = 0x%08X\n", scheduler, SCHED_FIFO);
     }
 
-    oboe::Result pause() {
-        LOGD("NativeAudioContext::%s() called", __func__);
-        oboe::Result result = oboe::Result::OK;
-        stopBlockingIOThread();
-        for (int32_t i = 0; i < kMaxStreams; i++) {
-            oboe::AudioStream *oboeStream = mOboeStreams[i];
-            if (oboeStream != nullptr) {
-                result = oboeStream->requestPause();
-                printScheduler();
-            }
-        }
-        return result;
+    virtual void configureForStart() {}
+
+    oboe::Result start();
+
+    oboe::Result pause();
+
+    oboe::Result stopAllStreams();
+
+    virtual oboe::Result stop() {
+        return stopAllStreams();
     }
 
-    oboe::Result stopAudio() {
-        LOGD("NativeAudioContext::%s() called", __func__);
-        oboe::Result result = oboe::Result::OK;
-        stopBlockingIOThread();
+    virtual void setAmplitude(double amplitude) {}
 
-        for (int32_t i = 0; i < kMaxStreams; i++) {
-            oboe::AudioStream *oboeStream = mOboeStreams[i];
-            if (oboeStream != nullptr) {
-                result = oboeStream->requestStop();
-                printScheduler();
-            }
-        }
-        LOGD("NativeAudioContext::%s() returns %d", __func__, result);
-        return result;
+    virtual oboe::Result startPlayback() {
+        return oboe::Result::OK;
     }
 
-    oboe::Result stopPlayback() {
-        LOGD("NativeAudioContext::%s() called", __func__);
-        oboe::Result result = oboe::Result::OK;
-        if (playbackStream != nullptr) {
-            result = playbackStream->requestStop();
-            playbackStream->close();
-            mPlayRecordingCallback.setRecording(nullptr);
-            delete playbackStream;
-            playbackStream = nullptr;
-        }
-        return result;
+    virtual oboe::Result stopPlayback() {
+        return oboe::Result::OK;
     }
 
-    oboe::Result stop() {
-        oboe::Result resultStopPlayback = stopPlayback();
-        oboe::Result resultStopAudio = stopAudio();
+    virtual void runBlockingIO() {};
 
-        LOGD("NativeAudioContext::%s() stop modules", __func__);
-        for (int i = 0; i < mChannelCount; i++) {
-            sineOscillators[i].stop();
-            sawtoothOscillators[i].stop();
-        }
-        impulseGenerator.stop();
-        sawPingGenerator.stop();
-        if (mSinkFloat) {
-            mSinkFloat->stop();
-        }
-        if (mSinkI16) {
-            mSinkI16->stop();
-        }
-
-        oboe::Result result = (resultStopPlayback != oboe::Result::OK)
-                ? resultStopPlayback
-                : resultStopAudio;
-
-        LOGD("NativeAudioContext::%s() returns %d", __func__, result);
-        return result;
-    }
-
-    oboe::Result startPlayback() {
-        stop();
-        LOGD("NativeAudioContext::%s() called", __func__);
-        oboe::AudioStreamBuilder builder;
-        builder.setChannelCount(mChannelCount)
-                ->setSampleRate(mSampleRate)
-                ->setFormat(oboe::AudioFormat::Float)
-                ->setCallback(&mPlayRecordingCallback)
-                ->setAudioApi(oboe::AudioApi::OpenSLES);
-        oboe::Result result = builder.openStream(&playbackStream);
-        LOGD("NativeAudioContext::startPlayback() openStream() returned %d", result);
-        if (result != oboe::Result::OK) {
-            delete playbackStream;
-            playbackStream = nullptr;
-        } else if (playbackStream != nullptr) {
-            if (mRecording != nullptr) {
-                mRecording->rewind();
-                mPlayRecordingCallback.setRecording(mRecording.get());
-                result = playbackStream->requestStart();
-            }
-        }
-        return result;
-    }
-
-    static void threadCallback(NativeAudioContext *context) {
+    static void threadCallback(ActivityContext *context) {
         LOGD("%s: called", __func__);
         context->runBlockingIO();
         LOGD("%s: exiting", __func__);
     }
 
-    oboe::Result start();
-
-    void setToneEnabled(bool enabled) {
-        LOGD("%s(%d)", __func__, enabled ? 1 : 0);
-        sawPingGenerator.setEnabled(enabled);
-    }
-
-    void setAmplitude(double amplitude) {
-        LOGD("%s(%f)", __func__, amplitude);
-        for (int i = 0; i < mChannelCount; i++) {
-            sineOscillators[i].amplitude.setValue(amplitude);
-            sawtoothOscillators[i].amplitude.setValue(amplitude);
+    void stopBlockingIOThread() {
+        if (dataThread != nullptr) {
+            // stop a thread that runs in place of the callback
+            threadEnabled.store(false); // ask thread to exit its loop
+            dataThread->join();
+            dataThread = nullptr;
         }
-        sawPingGenerator.amplitude.setValue(amplitude);
-        impulseGenerator.amplitude.setValue(amplitude);
     }
 
-    void setCallbackReturnStop(bool b) {
-        oboeCallbackProxy.setCallbackReturnStop(b);
+    virtual double getPeakLevel(int index) {
+        return 0.0;
+    }
+
+    virtual void setEnabled(bool enabled) {
+    }
+
+    bool isMMapUsed(int32_t streamIndex);
+
+    int32_t getFramesPerBlock() {
+        return (callbackSize == 0) ? mFramesPerBurst : callbackSize;
     }
 
     int64_t getCallbackCount() {
         return oboeCallbackProxy.getCallbackCount();
     }
 
-    void setChannelEnabled(int channelIndex, bool enabled);
 
-    void setActivityType(int activityType) {
-        LOGD("%s(%d)", __func__, activityType);
-        mActivityType = (ActivityType) activityType;
-    }
+    virtual void setChannelEnabled(int channelIndex, bool enabled) {}
 
-    double getPeakLevel(int index) {
+    static bool   useCallback;
+    static bool   callbackReturnStop;
+    static int    callbackSize;
+
+
+protected:
+    oboe::AudioStream * getInputStream();
+    oboe::AudioStream * getOutputStream();
+    int32_t allocateStreamIndex();
+    void freeStreamIndex(int32_t streamIndex);
+
+    virtual void finishOpen(bool isInput, oboe::AudioStream *oboeStream) {}
+
+    virtual oboe::Result startStreams() = 0;
+
+    std::unique_ptr<float []>    dataBuffer{};
+
+    AudioStreamGateway           audioStreamGateway;
+    OboeStreamCallbackProxy      oboeCallbackProxy;
+
+    static constexpr int         kMaxStreams = 8;
+    oboe::AudioStream           *mOboeStreams[kMaxStreams]{};
+    int32_t                      mFramesPerBurst = 0; // TODO per stream
+    int32_t                      mChannelCount = 0; // TODO per stream
+    int32_t                      mSampleRate = 0; // TODO per stream
+
+    std::atomic<bool>            threadEnabled{false};
+    std::thread                 *dataThread = nullptr;
+
+    bool                       (*mAAudioStream_isMMap)(AAudioStream *stream) = nullptr;
+    void                        *mLibHandle = nullptr;
+
+private:
+};
+
+/**
+ * Test a single input stream.
+ */
+class ActivityTestInput : public ActivityContext {
+public:
+
+    ActivityTestInput() {}
+    virtual ~ActivityTestInput() = default;
+
+    void configureForStart() override;
+
+    double getPeakLevel(int index) override {
         return mInputAnalyzer.getPeakLevel(index);
     }
 
-    bool                         useCallback = true;
-    bool                         callbackReturnStop = false;
-    int                          callbackSize = 0;
+    void runBlockingIO() override;
+
+    std::unique_ptr<MultiChannelRecording>  mRecording{};
+
+    InputStreamCallbackAnalyzer  mInputAnalyzer;
+
+protected:
+    oboe::Result startStreams() override {
+        return getInputStream()->requestStart();
+    }
 
 private:
+};
+
+/**
+ * Record a configured input stream and play it back some simple way.
+ */
+class ActivityRecording : public ActivityTestInput {
+public:
+
+    ActivityRecording() {}
+    virtual ~ActivityRecording() = default;
+
+    oboe::Result stop() override {
+
+        oboe::Result resultStopPlayback = stopPlayback();
+        oboe::Result resultStopAudio = ActivityContext::stop();
+
+        oboe::Result result = (resultStopPlayback != oboe::Result::OK)
+                              ? resultStopPlayback
+                              : resultStopAudio;
+        return result;
+    }
+
+    oboe::Result startPlayback() override;
+
+    oboe::Result stopPlayback() override;
+
+    PlayRecordingCallback        mPlayRecordingCallback;
+    oboe::AudioStream           *playbackStream = nullptr;
+
+};
+
+/**
+ * Test a single output stream.
+ */
+class ActivityTestOutput : public ActivityContext {
+public:
+    ActivityTestOutput()
+            : sineOscillators(MAX_SINE_OSCILLATORS)
+            , sawtoothOscillators(MAX_SINE_OSCILLATORS) {}
+
+    virtual ~ActivityTestOutput() = default;
+
+    void close(int32_t streamIndex) override;
+
+    oboe::Result startStreams() override {
+        return getOutputStream()->requestStart();
+    }
+
+    void configureForStart() override;
+
+    virtual void configureStreamGateway();
+
+    void runBlockingIO() override;
+
+    void setAmplitude(double amplitude) override {
+        LOGD("%s(%f)", __func__, amplitude);
+        for (int i = 0; i < mChannelCount; i++) {
+            sineOscillators[i].amplitude.setValue(amplitude);
+            sawtoothOscillators[i].amplitude.setValue(amplitude);
+        }
+        impulseGenerator.amplitude.setValue(amplitude);
+    }
+
+    void setChannelEnabled(int channelIndex, bool enabled) override;
 
     // WARNING - must match order in strings.xml and OboeAudioOutputStream.java
     enum ToneType {
@@ -253,6 +289,98 @@
         Sawtooth = 3
     };
 
+protected:
+    ToneType                     mToneType = ToneType::Sine;
+    std::vector<SineOscillator>  sineOscillators;
+    std::vector<SawtoothOscillator>  sawtoothOscillators;
+
+    ImpulseOscillator            impulseGenerator;
+
+    std::unique_ptr<ManyToMultiConverter>   manyToMulti;
+    std::unique_ptr<MonoToMultiConverter>   monoToMulti;
+    std::shared_ptr<flowgraph::SinkFloat>   mSinkFloat;
+    std::shared_ptr<flowgraph::SinkI16>     mSinkI16;
+};
+
+/**
+ * Generate a short beep with a very short attack.
+ * This is used by Java to measure output latency.
+ */
+class ActivityTapToTone : public ActivityTestOutput {
+public:
+    ActivityTapToTone() {}
+    virtual ~ActivityTapToTone() = default;
+
+    void configureForStart() override;
+
+    void setAmplitude(double amplitude) override {
+        LOGD("%s(%f)", __func__, amplitude);
+        ActivityTestOutput::setAmplitude(amplitude);
+        sawPingGenerator.amplitude.setValue(amplitude);
+    }
+
+    virtual void setEnabled(bool enabled) override {
+        sawPingGenerator.setEnabled(enabled);
+    }
+
+    SawPingGenerator             sawPingGenerator;
+};
+
+/**
+ * Echo input to outp[ut through a delay line.
+ */
+class ActivityEcho : public ActivityContext {
+public:
+
+    oboe::Result startStreams() override {
+        return mFullDuplexEcho->start();
+    }
+
+    void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
+
+protected:
+    void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override;
+
+private:
+    std::unique_ptr<FullDuplexEcho>   mFullDuplexEcho{};
+};
+
+/**
+ * Switch between various
+ */
+class NativeAudioContext {
+public:
+
+    ActivityContext *getCurrentActivity() {
+        return currentActivity;
+    };
+
+    void setActivityType(int activityType) {
+        LOGD("%s(%d)", __func__, activityType);
+        mActivityType = (ActivityType) activityType;
+        switch(mActivityType) {
+            default:
+            case ActivityType::Undefined:
+            case ActivityType::TestOutput:
+                currentActivity = &mActivityTestOutput;
+                break;
+            case ActivityType::TestInput:
+                currentActivity = &mActivityTestInput;
+                break;
+            case ActivityType::TapToTone:
+                currentActivity = &mActivityTapToTone;
+                break;
+            case ActivityType::RecordPlay:
+                currentActivity = &mActivityRecording;
+                break;
+            case ActivityType::Echo:
+                currentActivity = &mActivityEcho;
+                break;
+        }
+    }
+
+private:
+
     // WARNING - must match definitions in TestAudioActivity.java
     enum ActivityType {
         Undefined = -1,
@@ -263,65 +391,14 @@
         Echo = 4
     };
 
-    oboe::AudioStream * getInputStream();
-    oboe::AudioStream * getOutputStream();
-
-    int32_t allocateStreamIndex();
-
-    void configureForActivityType();
-
-    void freeStreamIndex(int32_t streamIndex);
-
-    void connectTone();
-
-    void runBlockingIO();
-
-    void stopBlockingIOThread() {
-        if (!useCallback) {
-            // stop a thread that runs in place of the callback
-            threadEnabled.store(false); // ask thread to exit its loop
-            if (dataThread != nullptr) {
-                dataThread->join();
-                dataThread = nullptr;
-            }
-        }
-
-    }
-
-    static constexpr int         kMaxStreams = 8;
-    oboe::AudioStream           *mOboeStreams[kMaxStreams]{};
-    int32_t                      mFramesPerBurst = 0; // TODO per stream
-    int32_t                      mChannelCount = 0; // TODO per stream
-    int32_t                      mSampleRate = 0; // TODO per stream
-    ToneType                     mToneType = ToneType::Sine;
     ActivityType                 mActivityType = ActivityType::Undefined;
+    ActivityTestOutput           mActivityTestOutput;
+    ActivityTestInput            mActivityTestInput;
+    ActivityTapToTone            mActivityTapToTone;
+    ActivityRecording            mActivityRecording;
+    ActivityEcho                 mActivityEcho;
 
-    std::atomic<bool>            threadEnabled{false};
-    std::thread                 *dataThread = nullptr;
-
-    OboeStreamCallbackProxy      oboeCallbackProxy;
-    AudioStreamGateway           audioStreamGateway;
-    InputStreamCallbackAnalyzer  mInputAnalyzer;
-
-    std::vector<SineOscillator>  sineOscillators;
-    std::vector<SawtoothOscillator>  sawtoothOscillators;
-
-    ImpulseOscillator            impulseGenerator;
-    SawPingGenerator             sawPingGenerator;
-    oboe::AudioStream           *playbackStream = nullptr;
-
-    std::unique_ptr<float []>               dataBuffer{};
-    std::unique_ptr<ManyToMultiConverter>   manyToMulti;
-    std::unique_ptr<MonoToMultiConverter>   monoToMulti;
-    std::shared_ptr<flowgraph::SinkFloat>   mSinkFloat;
-    std::shared_ptr<flowgraph::SinkI16>     mSinkI16;
-    std::unique_ptr<MultiChannelRecording>  mRecording{};
-    std::unique_ptr<FullDuplexEcho>         mFullDuplexEcho{};
-
-    PlayRecordingCallback        mPlayRecordingCallback;
-
-    bool                       (*mAAudioStream_isMMap)(AAudioStream *stream) = nullptr;
-    void                        *mLibHandle = nullptr;
+    ActivityContext             *currentActivity = &mActivityTestOutput;
 
 };
 
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index be75f63..2779c6b 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -27,7 +27,7 @@
 
 #include "NativeAudioContext.h"
 
-static NativeAudioContext engine;
+NativeAudioContext engine;
 
 /*********************************************************************************/
 /**********************  JNI  Prototypes *****************************************/
@@ -48,14 +48,7 @@
                                                        jboolean isInput);
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_close(JNIEnv *env, jobject, jint);
-/*
-JNIEXPORT void JNICALL
-Java_com_google_sample_oboe_manualtest_TestAudioActivity_startNative(JNIEnv *env, jobject);
-JNIEXPORT void JNICALL
-Java_com_google_sample_oboe_manualtest_TestAudioActivity_pauseNative(JNIEnv *env, jobject);
-JNIEXPORT void JNICALL
-Java_com_google_sample_oboe_manualtest_TestAudioActivity_stopNative(JNIEnv *env, jobject);
-*/
+
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_setThresholdInFrames(JNIEnv *env, jobject, jint, jint);
 JNIEXPORT jint JNICALL
@@ -104,7 +97,7 @@
         jboolean isInput) {
     LOGD("OboeAudioStream_openNative: sampleRate = %d, framesPerBurst = %d", sampleRate, framesPerBurst);
 
-    return (jint) engine.open(nativeApi,
+    return (jint) engine.getCurrentActivity()->open(nativeApi,
                               sampleRate,
                               channelCount,
                               format,
@@ -118,33 +111,33 @@
 
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_TestAudioActivity_startNative(JNIEnv *env, jobject) {
-    return (jint) engine.start();
+    return (jint) engine.getCurrentActivity()->start();
 }
 
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_TestAudioActivity_pauseNative(JNIEnv *env, jobject) {
-    return (jint) engine.pause();
+    return (jint) engine.getCurrentActivity()->pause();
 }
 
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_TestAudioActivity_stopNative(JNIEnv *env, jobject) {
-    return (jint) engine.stop();
+    return (jint) engine.getCurrentActivity()->stop();
 }
 
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_startPlaybackNative(JNIEnv *env, jobject) {
-    return (jint) engine.startPlayback();
+    return (jint) engine.getCurrentActivity()->startPlayback();
 }
 
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_close(JNIEnv *env, jobject, jint streamIndex) {
-    engine.close(streamIndex);
+    engine.getCurrentActivity()->close(streamIndex);
 }
 
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_setBufferSizeInFrames(
         JNIEnv *env, jobject, jint streamIndex, jint threshold) {
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         auto result = oboeStream->setBufferSizeInFrames(threshold);
         return (!result)
@@ -158,7 +151,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getBufferSizeInFrames(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getBufferSizeInFrames();
     }
@@ -169,7 +162,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getBufferCapacityInFrames(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getBufferCapacityInFrames();
     }
@@ -193,7 +186,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getNativeApi(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         oboe::AudioApi audioApi = oboeStream->getAudioApi();
         result = convertAudioApiToNativeApi(audioApi);
@@ -206,7 +199,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSampleRate(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getSampleRate();
     }
@@ -217,7 +210,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSharingMode(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = (jint) oboeStream->getSharingMode();
     }
@@ -228,7 +221,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getPerformanceMode(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = (jint) oboeStream->getPerformanceMode();
     }
@@ -239,7 +232,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesPerBurst(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getFramesPerBurst();
     }
@@ -250,7 +243,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getChannelCount(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getChannelCount();
     }
@@ -260,7 +253,7 @@
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFormat(JNIEnv *env, jobject instance, jint streamIndex) {
         jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = (jint) oboeStream->getFormat();
     }
@@ -271,7 +264,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getDeviceId(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getDeviceId();
     }
@@ -282,7 +275,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSessionId(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getSessionId();
     }
@@ -293,7 +286,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesWritten(
         JNIEnv *env, jobject, jint streamIndex) {
     jlong result = (jint) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getFramesWritten();
     }
@@ -304,7 +297,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesRead(
         JNIEnv *env, jobject, jint streamIndex) {
     jlong result = (jlong) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         result = oboeStream->getFramesRead();
     }
@@ -315,7 +308,7 @@
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getXRunCount(
         JNIEnv *env, jobject, jint streamIndex) {
     jint result = (jlong) oboe::Result::ErrorNull;
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         auto oboeResult  = oboeStream->getXRunCount();
         if (!oboeResult) {
@@ -330,12 +323,12 @@
 JNIEXPORT jlong JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getCallbackCount(
         JNIEnv *env, jobject) {
-    return engine.getCallbackCount();
+    return engine.getCurrentActivity()->getCallbackCount();
 }
 
 JNIEXPORT jdouble JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getLatency(JNIEnv *env, jobject instance, jint streamIndex) {
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         auto result = oboeStream->calculateLatencyMillis();
         return (!result) ? -1.0 : result.value();
@@ -345,7 +338,7 @@
 
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_getState(JNIEnv *env, jobject instance, jint streamIndex) {
-    oboe::AudioStream *oboeStream = engine.getStream(streamIndex);
+    oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
     if (oboeStream != nullptr) {
         auto state = oboeStream->getState();
         if (state != oboe::StreamState::Starting && state != oboe::StreamState::Started) {
@@ -363,30 +356,30 @@
 Java_com_google_sample_oboe_manualtest_AudioInputTester_getPeakLevel(JNIEnv *env,
                                                           jobject instance,
                                                           jint index) {
-    return engine.getPeakLevel(index);
+    return engine.getCurrentActivity()->getPeakLevel(index);
 }
 
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_setUseCallback(JNIEnv *env, jclass type,
                                                                       jboolean useCallback) {
-    engine.useCallback = useCallback;
+    ActivityContext::useCallback = useCallback;
 }
 
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_setCallbackReturnStop(JNIEnv *env, jclass type,
                                                                       jboolean b) {
-    engine.setCallbackReturnStop(b);
+    ActivityContext::callbackReturnStop = b;
 }
 
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_setCallbackSize(JNIEnv *env, jclass type,
                                                             jint callbackSize) {
-    engine.callbackSize = callbackSize;
+    ActivityContext::callbackSize = callbackSize;
 }
 
 JNIEXPORT jboolean JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_isMMap(JNIEnv *env, jobject instance, jint streamIndex) {
-    return engine.isMMapUsed(streamIndex);
+    return engine.getCurrentActivity()->isMMapUsed(streamIndex);
 }
 
 // ================= OboeAudioOutputStream ================================
@@ -394,26 +387,26 @@
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setToneEnabled(
         JNIEnv *env, jobject, jboolean enabled) {
-    engine.setToneEnabled(enabled);
+    engine.getCurrentActivity()->setEnabled(enabled);
 }
 
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setToneType(
         JNIEnv *env, jobject, jint toneType) {
-    engine.setToneType(toneType);
+// FIXME    engine.getCurrentActivity()->setToneType(toneType);
 }
 
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setAmplitude(
         JNIEnv *env, jobject, jdouble amplitude) {
-    engine.setAmplitude(amplitude);
+    engine.getCurrentActivity()->setAmplitude(amplitude);
 
 }
 
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setChannelEnabled(
         JNIEnv *env, jobject, jint channelIndex, jboolean enabled) {
-    engine.setChannelEnabled(channelIndex, enabled);
+    engine.getCurrentActivity()->setChannelEnabled(channelIndex, enabled);
 }