OboeTester: add checkbox for MMAP mode
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index 4538f26..2f2e7e5 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -100,26 +100,9 @@
 
 bool ActivityContext::isMMapUsed(int32_t streamIndex) {
     oboe::AudioStream *oboeStream = getStream(streamIndex);
-    if (oboeStream != nullptr && oboeStream->usesAAudio()) {
-        if (mAAudioStream_isMMap == nullptr) {
-            mLibHandle = dlopen(LIB_AAUDIO_NAME, 0);
-            if (mLibHandle == nullptr) {
-                LOGI("%s() could not find " LIB_AAUDIO_NAME, __func__);
-                return false;
-            }
-
-            mAAudioStream_isMMap = (bool (*)(AAudioStream *stream))
-                    dlsym(mLibHandle, FUNCTION_IS_MMAP);
-
-            if(mAAudioStream_isMMap == nullptr) {
-                LOGI("%s() could not find " FUNCTION_IS_MMAP, __func__);
-                return false;
-            }
-        }
-        AAudioStream *aaudioStream = (AAudioStream *) oboeStream->getUnderlyingStream();
-        return mAAudioStream_isMMap(aaudioStream);
-    }
-    return false;
+    if (oboeStream == nullptr) return false;
+    if (oboeStream->getAudioApi() != AudioApi::AAudio) return false;
+    return AAudioExtensions::getInstance().isMMapUsed(oboeStream);
 }
 
 oboe::Result ActivityContext::pause() {
@@ -159,20 +142,20 @@
     }
 }
 
-int ActivityContext::open(
-        jint nativeApi,
-        jint sampleRate,
-        jint channelCount,
-        jint format,
-        jint sharingMode,
-        jint performanceMode,
-        jint deviceId,
-        jint sessionId,
-        jint framesPerBurst,
-        jboolean channelConversionAllowed,
-        jboolean formatConversionAllowed,
-        jint rateConversionQuality,
-        jboolean isInput) {
+int ActivityContext::open(jint nativeApi,
+                          jint sampleRate,
+                          jint channelCount,
+                          jint format,
+                          jint sharingMode,
+                          jint performanceMode,
+                          jint deviceId,
+                          jint sessionId,
+                          jint framesPerBurst,
+                          jboolean channelConversionAllowed,
+                          jboolean formatConversionAllowed,
+                          jint rateConversionQuality,
+                          jboolean isMMap,
+                          jboolean isInput) {
 
     oboe::AudioApi audioApi = oboe::AudioApi::Unspecified;
     switch (nativeApi) {
@@ -218,10 +201,15 @@
     }
     builder.setAudioApi(audioApi);
 
+    // Temporarily set the AAudio MMAP policy to disable MMAP or not.
+    bool oldMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
+    AAudioExtensions::getInstance().setMMapEnabled(isMMap);
+
     // 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);
+    AAudioExtensions::getInstance().setMMapEnabled(oldMMapEnabled);
     if (result != oboe::Result::OK) {
         delete oboeStream;
         oboeStream = nullptr;
@@ -239,6 +227,7 @@
         finishOpen(isInput, oboeStream);
     }
 
+
     if (!mUseCallback) {
         int numSamples = getFramesPerBlock() * mChannelCount;
         dataBuffer = std::make_unique<float[]>(numSamples);
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index 6022c81..7d6aa8b 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -66,12 +66,105 @@
 
 #define LIB_AAUDIO_NAME          "libaaudio.so"
 #define FUNCTION_IS_MMAP         "AAudioStream_isMMapUsed"
+#define FUNCTION_SET_MMAP_POLICY "AAudio_setMMapPolicy"
+#define FUNCTION_GET_MMAP_POLICY "AAudio_getMMapPolicy"
 
 #define SECONDS_TO_RECORD        10
 
 typedef struct AAudioStreamStruct         AAudioStream;
 
 /**
+ * Call some AAudio test routines that are not part of the normal API.
+ */
+class AAudioExtensions {
+public:
+    static AAudioExtensions &getInstance() {
+        static AAudioExtensions instance;
+        return instance;
+    }
+
+    bool isMMapUsed(oboe::AudioStream *oboeStream) {
+        if (!loadLibrary()) return false;
+        AAudioStream *aaudioStream = (AAudioStream *) oboeStream->getUnderlyingStream();
+        return mAAudioStream_isMMap(aaudioStream);
+    }
+
+    bool setMMapEnabled(bool enabled) {
+        if (!loadLibrary()) return false;
+        return mAAudio_setMMapPolicy(enabled ? AAUDIO_POLICY_AUTO : AAUDIO_POLICY_NEVER);
+    }
+
+    bool isMMapEnabled() {
+        if (!loadLibrary()) return false;
+        return mAAudio_getMMapPolicy() != AAUDIO_POLICY_NEVER;
+    }
+
+    bool isMMapSupported() {
+        if (!loadLibrary()) return false;
+        return mMMapSupported;
+    }
+
+private:
+
+    enum {
+        AAUDIO_POLICY_NEVER = 1,
+        AAUDIO_POLICY_AUTO,
+        AAUDIO_POLICY_ALWAYS
+    };
+    typedef int32_t aaudio_policy_t;
+
+    // return true if it succeeds
+    bool loadLibrary() {
+        if (mFirstTime) {
+            mFirstTime = false;
+            mLibHandle = dlopen(LIB_AAUDIO_NAME, 0);
+            if (mLibHandle == nullptr) {
+                LOGI("%s() could not find "
+                             LIB_AAUDIO_NAME, __func__);
+                return false;
+            }
+
+            mAAudioStream_isMMap = (bool (*)(AAudioStream *stream))
+                    dlsym(mLibHandle, FUNCTION_IS_MMAP);
+
+            if (mAAudioStream_isMMap == nullptr) {
+                LOGI("%s() could not find "
+                             FUNCTION_IS_MMAP, __func__);
+                return false;
+            }
+
+            mAAudio_setMMapPolicy = (int32_t (*)(aaudio_policy_t policy))
+                    dlsym(mLibHandle, FUNCTION_SET_MMAP_POLICY);
+
+            if (mAAudio_setMMapPolicy == nullptr) {
+                LOGI("%s() could not find "
+                             FUNCTION_SET_MMAP_POLICY, __func__);
+                return false;
+            }
+
+            mAAudio_getMMapPolicy = (aaudio_policy_t (*)())
+                    dlsym(mLibHandle, FUNCTION_GET_MMAP_POLICY);
+
+            if (mAAudio_getMMapPolicy == nullptr) {
+                LOGI("%s() could not find "
+                             FUNCTION_GET_MMAP_POLICY, __func__);
+                return false;
+            }
+
+            mMMapSupported = isMMapEnabled(); // still supported even if disabled later
+        }
+        return true;
+    }
+
+    bool     mFirstTime = true;
+    void     *mLibHandle = nullptr;
+    bool    (*mAAudioStream_isMMap)(AAudioStream *stream) = nullptr;
+    int32_t (*mAAudio_setMMapPolicy)(aaudio_policy_t policy) = nullptr;
+    aaudio_policy_t (*mAAudio_getMMapPolicy)() = nullptr;
+    bool      mMMapSupported = false;
+};
+
+/**
  * Abstract base class that corresponds to a test at the Java level.
  */
 class ActivityContext {
@@ -103,6 +196,7 @@
              jboolean channelConversionAllowed,
              jboolean formatConversionAllowed,
              jint rateConversionQuality,
+             jboolean isMMap,
              jboolean isInput);
 
 
@@ -213,9 +307,6 @@
     std::atomic<bool>            threadEnabled{false};
     std::thread                 *dataThread = nullptr;
 
-    bool                       (*mAAudioStream_isMMap)(AAudioStream *stream) = nullptr;
-    void                        *mLibHandle = nullptr;
-
 private:
 };
 
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index 24de205..2a5b13f 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -48,6 +48,7 @@
                                                        jboolean channelConversionAllowed,
                                                        jboolean formatConversionAllowed,
                                                        jint rateConversionQuality,
+                                                       jboolean isMMap,
                                                        jboolean isInput);
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_close(JNIEnv *env, jobject, jint);
@@ -85,6 +86,11 @@
 /**********************  JNI Implementations *************************************/
 /*********************************************************************************/
 
+JNIEXPORT jboolean JNICALL
+Java_com_google_sample_oboe_manualtest_NativeEngine_isMMapSupported(JNIEnv *env, jclass type) {
+    return AAudioExtensions::getInstance().isMMapSupported();
+}
+
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_OboeAudioStream_openNative(
         JNIEnv *env, jobject synth,
@@ -100,22 +106,24 @@
         jboolean channelConversionAllowed,
         jboolean formatConversionAllowed,
         jint rateConversionQuality,
+        jboolean isMMap,
         jboolean isInput) {
     LOGD("OboeAudioStream_openNative: sampleRate = %d, framesPerBurst = %d", sampleRate, framesPerBurst);
 
     return (jint) engine.getCurrentActivity()->open(nativeApi,
-                              sampleRate,
-                              channelCount,
-                              format,
-                              sharingMode,
-                              performanceMode,
-                              deviceId,
-                              sessionId,
-                              framesPerBurst,
-                              channelConversionAllowed,
-                              formatConversionAllowed,
-                              rateConversionQuality,
-                              isInput);
+                                                    sampleRate,
+                                                    channelCount,
+                                                    format,
+                                                    sharingMode,
+                                                    performanceMode,
+                                                    deviceId,
+                                                    sessionId,
+                                                    framesPerBurst,
+                                                    channelConversionAllowed,
+                                                    formatConversionAllowed,
+                                                    rateConversionQuality,
+                                                    isMMap,
+                                                    isInput);
 }
 
 JNIEXPORT jint JNICALL
@@ -541,4 +549,4 @@
     return engine.mActivityGlitches.getGlitchAnalyzer()->getPeakAmplitude();
 }
 
-}
\ No newline at end of file
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeEngine.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeEngine.java
new file mode 100644
index 0000000..592003d
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeEngine.java
@@ -0,0 +1,6 @@
+package com.google.sample.oboe.manualtest;
+
+public class NativeEngine {
+
+    static native boolean isMMapSupported();
+}
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 b9a96a7..a6110cf 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
@@ -68,6 +68,7 @@
                 requestedConfiguration.getChannelConversionAllowed(),
                 requestedConfiguration.getFormatConversionAllowed(),
                 requestedConfiguration.getRateConversionQuality(),
+                requestedConfiguration.isMMap(),
                 isInput()
         );
         if (result < 0) {
@@ -105,6 +106,7 @@
             boolean channelConversionAllowed,
             boolean formatConversionAllowed,
             int rateConversionQuality,
+            boolean isMMap,
             boolean isInput);
 
     @Override
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java
index 3b372cb..b19623a 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java
@@ -69,6 +69,7 @@
     private int mRateConversionQuality;
 
     private int mFramesPerBurst = 0;
+
     private boolean mMMap = false;
 
     public StreamConfiguration() {
@@ -93,6 +94,7 @@
         mChannelConversionAllowed = false;
         mFormatConversionAllowed = false;
         mRateConversionQuality = RATE_CONVERSION_QUALITY_NONE;
+        mMMap = NativeEngine.isMMapSupported();
     }
 
     public int getFramesPerBurst() {
@@ -273,4 +275,5 @@
     public int getRateConversionQuality() {
         return mRateConversionQuality;
     }
+
 }
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java
index 9cd54b0..2f7fe99 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java
@@ -44,6 +44,8 @@
     protected Spinner mNativeApiSpinner;
     private TextView mActualNativeApiView;
 
+    private TextView mActualMMapView;
+    private CheckBox mRequestedMMapView;
     private TextView mActualExclusiveView;
     private TextView mActualPerformanceView;
     private Spinner  mPerformanceSpinner;
@@ -80,6 +82,10 @@
         }
     };
 
+    public static String yesOrNo(boolean b) {
+        return b ?  "YES" : "NO";
+    }
+
     private void updateSettingsViewText() {
         if (mHideableView.isShown()) {
             mOptionExpander.setText(mHideSettingsText);
@@ -162,6 +168,18 @@
             }
         });
 
+        mActualMMapView = (TextView) findViewById(R.id.actualMMap);
+        mRequestedMMapView = (CheckBox) findViewById(R.id.requestedMMapEnable);
+        mRequestedMMapView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mRequestedConfiguration.setMMap(mRequestedMMapView.isChecked());
+            }
+        });
+        boolean mmapSupported = NativeEngine.isMMapSupported();
+        mRequestedMMapView.setEnabled(mmapSupported);
+        mRequestedMMapView.setChecked(mmapSupported);
+
         mActualExclusiveView = (TextView) findViewById(R.id.actualExclusiveMode);
         mRequestedExclusiveView = (CheckBox) findViewById(R.id.requestedExclusiveMode);
         mRequestedExclusiveView.setOnClickListener(new View.OnClickListener() {
@@ -337,8 +355,10 @@
         value = mActualConfiguration.getNativeApi();
         mActualNativeApiView.setText(StreamConfiguration.convertNativeApiToText(value));
 
-        value = mActualConfiguration.getSharingMode();
-        mActualExclusiveView.setText(StreamConfiguration.convertSharingModeToText(value));
+        mActualMMapView.setText(yesOrNo(mActualConfiguration.isMMap()));
+        int sharingMode = mActualConfiguration.getSharingMode();
+        boolean isExclusive = (sharingMode == StreamConfiguration.SHARING_MODE_EXCLUSIVE);
+        mActualExclusiveView.setText(yesOrNo(isExclusive));
 
         value = mActualConfiguration.getPerformanceMode();
         mActualPerformanceView.setText(StreamConfiguration.convertPerformanceModeToText(value));
@@ -352,10 +372,12 @@
         mActualSampleRateView.setText(mActualConfiguration.getSampleRate() + "");
         mActualSessionIdView.setText("S#: " + mActualConfiguration.getSessionId());
 
+        boolean isMMap = mActualConfiguration.isMMap();
         mStreamInfoView.setText("burst = " + mActualConfiguration.getFramesPerBurst()
                 + ", capacity = " + mActualConfiguration.getBufferCapacityInFrames()
                 + ", devID = " + mActualConfiguration.getDeviceId()
                 + ", " + (mActualConfiguration.isMMap() ? "MMAP" : "Legacy")
+                + (isMMap ? ", " + StreamConfiguration.convertSharingModeToText(sharingMode) : "")
         );
 
         mHideableView.requestLayout();
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml b/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml
index e16e3af..c727096 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml
@@ -68,7 +68,7 @@
         android:id="@+id/text_analyzer_result"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:lines="8"
+        android:lines="12"
         android:text="@string/auto_glitch_instructions"
         android:textSize="18sp"
         android:textStyle="bold"
diff --git a/apps/OboeTester/app/src/main/res/layout/stream_config.xml b/apps/OboeTester/app/src/main/res/layout/stream_config.xml
index 27d2153..70435c2 100644
--- a/apps/OboeTester/app/src/main/res/layout/stream_config.xml
+++ b/apps/OboeTester/app/src/main/res/layout/stream_config.xml
@@ -44,6 +44,22 @@
                     android:text="\?" />
             </TableRow>
 
+            <TableRow
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="Device: " />
+
+                <com.google.sample.audio_device.AudioDeviceSpinner
+                    android:id="@+id/devices_spinner"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"/>
+
+            </TableRow>
+
             <TableRow>
 
                 <TextView
@@ -131,22 +147,62 @@
                     android:text="\?" />
 
             </TableRow>
-            <TableRow
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
+        </TableLayout>
 
-                <TextView
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+                <CheckBox
+                    android:id="@+id/requestedMMapEnable"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:text="Device: " />
+                    android:layout_marginRight="8sp"
+                    android:text="MMAP" />
 
-                <com.google.sample.audio_device.AudioDeviceSpinner
-                    android:id="@+id/devices_spinner"
+                <TextView
+                    android:id="@+id/actualMMap"
                     android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"/>
+                    android:layout_height="wrap_content"
+                    android:layout_marginRight="12sp"
+                    android:text="\?" />
 
-            </TableRow>
-        </TableLayout>
+                <CheckBox
+                    android:id="@+id/requestedExclusiveMode"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginRight="8sp"
+                    android:text="Exclusive" />
+
+                <TextView
+                    android:id="@+id/actualExclusiveMode"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginRight="15sp"
+                    android:text="\?" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <CheckBox
+                android:id="@+id/requestAudioEffect"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginRight="10sp"
+                android:text="Effect" />
+
+            <TextView
+                android:id="@+id/sessionId"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="\?" />
+
+        </LinearLayout>
 
         <LinearLayout
             android:layout_width="match_parent"
@@ -181,44 +237,6 @@
 
         </LinearLayout>
 
-        <TableLayout
-            android:id="@+id/flagTable"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:shrinkColumns="*"
-            >
-            <TableRow>
-
-                <CheckBox
-                    android:id="@+id/requestedExclusiveMode"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_marginRight="30sp"
-                    android:text="Exclusive" />
-
-                <TextView
-                    android:id="@+id/actualExclusiveMode"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:text="\?" />
-
-
-                <CheckBox
-                    android:id="@+id/requestAudioEffect"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_marginRight="30sp"
-                    android:text="Effect" />
-
-                <TextView
-                    android:id="@+id/sessionId"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:text="\?" />
-
-            </TableRow>
-
-        </TableLayout>
     </LinearLayout>