MediaSyncEvent: Fix thread safety

Test: atest synchronizedrecordstate_tests
Test: atest MediaSyncEventTest#testSynchronizedRecord
Bug: 281168109
Change-Id: I75677947ab2b508d20d55256187830c3ace9c9e9
diff --git a/services/audioflinger/AudioFlinger.h b/services/audioflinger/AudioFlinger.h
index 6b50693..a470478 100644
--- a/services/audioflinger/AudioFlinger.h
+++ b/services/audioflinger/AudioFlinger.h
@@ -90,6 +90,7 @@
 #include <sounddose/SoundDoseManager.h>
 #include <timing/MonotonicFrameCounter.h>
 #include <timing/SyncEvent.h>
+#include <timing/SynchronizedRecordState.h>
 
 #include "FastCapture.h"
 #include "FastMixer.h"
diff --git a/services/audioflinger/RecordTracks.h b/services/audioflinger/RecordTracks.h
index 868acfc..d91a210 100644
--- a/services/audioflinger/RecordTracks.h
+++ b/services/audioflinger/RecordTracks.h
@@ -109,10 +109,8 @@
             // be dropped and therefore not read by the application.
             sp<audioflinger::SyncEvent>        mSyncStartEvent;
 
-            // number of captured frames to drop after the start sync event has been received.
-            // when < 0, maximum frames to drop before starting capture even if sync event is
-            // not received
-            ssize_t                             mFramesToDrop;
+            audioflinger::SynchronizedRecordState
+                    mSynchronizedRecordState{mSampleRate}; // sampleRate defined in base
 
             // used by resampler to find source frames
             ResamplerBufferProvider            *mResamplerBufferProvider;
diff --git a/services/audioflinger/Threads.cpp b/services/audioflinger/Threads.cpp
index 06db915..b7f9d76 100644
--- a/services/audioflinger/Threads.cpp
+++ b/services/audioflinger/Threads.cpp
@@ -8469,7 +8469,11 @@
                     overrun = OVERRUN_FALSE;
                 }
 
-                if (activeTrack->mFramesToDrop == 0) {
+                // MediaSyncEvent handling: Synchronize AudioRecord to AudioTrack completion.
+                const ssize_t framesToDrop =
+                        activeTrack->mSynchronizedRecordState.updateRecordFrames(framesOut);
+                if (framesToDrop == 0) {
+                    // no sync event, process normally, otherwise ignore.
                     if (framesOut > 0) {
                         activeTrack->mSink.frameCount = framesOut;
                         // Sanitize before releasing if the track has no access to the source data
@@ -8479,28 +8483,7 @@
                         }
                         activeTrack->releaseBuffer(&activeTrack->mSink);
                     }
-                } else {
-                    // FIXME could do a partial drop of framesOut
-                    if (activeTrack->mFramesToDrop > 0) {
-                        activeTrack->mFramesToDrop -= (ssize_t)framesOut;
-                        if (activeTrack->mFramesToDrop <= 0) {
-                            activeTrack->clearSyncStartEvent();
-                        }
-                    } else {
-                        activeTrack->mFramesToDrop += framesOut;
-                        if (activeTrack->mFramesToDrop >= 0 || activeTrack->mSyncStartEvent == 0 ||
-                                activeTrack->mSyncStartEvent->isCancelled()) {
-                            ALOGW("Synced record %s, session %d, trigger session %d",
-                                  (activeTrack->mFramesToDrop >= 0) ? "timed out" : "cancelled",
-                                  activeTrack->sessionId(),
-                                  (activeTrack->mSyncStartEvent != 0) ?
-                                          activeTrack->mSyncStartEvent->triggerSession() :
-                                          AUDIO_SESSION_NONE);
-                            activeTrack->clearSyncStartEvent();
-                        }
-                    }
                 }
-
                 if (framesOut == 0) {
                     break;
                 }
@@ -8833,20 +8816,10 @@
     if (event == AudioSystem::SYNC_EVENT_NONE) {
         recordTrack->clearSyncStartEvent();
     } else if (event != AudioSystem::SYNC_EVENT_SAME) {
-        recordTrack->mSyncStartEvent = mAudioFlinger->createSyncEvent(event,
-                                       triggerSession,
-                                       recordTrack->sessionId(),
-                                       syncStartEventCallback,
-                                       recordTrack);
-        // Sync event can be cancelled by the trigger session if the track is not in a
-        // compatible state in which case we start record immediately
-        if (recordTrack->mSyncStartEvent->isCancelled()) {
-            recordTrack->clearSyncStartEvent();
-        } else {
-            // do not wait for the event for more than AudioSystem::kSyncRecordStartTimeOutMs
-            recordTrack->mFramesToDrop = -(ssize_t)
-                    ((AudioSystem::kSyncRecordStartTimeOutMs * recordTrack->mSampleRate) / 1000);
-        }
+        recordTrack->mSynchronizedRecordState.startRecording(
+                mAudioFlinger->createSyncEvent(
+                        event, triggerSession,
+                        recordTrack->sessionId(), syncStartEventCallback, recordTrack));
     }
 
     {
diff --git a/services/audioflinger/Tracks.cpp b/services/audioflinger/Tracks.cpp
index d3bf699..00c88bc 100644
--- a/services/audioflinger/Tracks.cpp
+++ b/services/audioflinger/Tracks.cpp
@@ -1681,6 +1681,7 @@
 {
     for (auto it = mSyncEvents.begin(); it != mSyncEvents.end();) {
         if ((*it)->type() == type) {
+            ALOGV("%s: triggering SyncEvent type %d", __func__, type);
             (*it)->trigger();
             it = mSyncEvents.erase(it);
         } else {
@@ -1931,6 +1932,8 @@
         }
     }
 
+    ALOGV("%s: trackFramesReleased:%lld  sinkFramesWritten:%lld  setDrained: %d",
+        __func__, (long long)trackFramesReleased, (long long)sinkFramesWritten, drained);
     mAudioTrackServerProxy->setDrained(drained);
     // Set correction for flushed frames that are not accounted for in released.
     local.mFlushed = mAudioTrackServerProxy->framesFlushed();
@@ -2505,7 +2508,6 @@
                   type, portId,
                   std::string(AMEDIAMETRICS_KEY_PREFIX_AUDIO_RECORD) + std::to_string(portId)),
         mOverflow(false),
-        mFramesToDrop(0),
         mResamplerBufferProvider(NULL), // initialize in case of early constructor exit
         mRecordBufferConverter(NULL),
         mFlags(flags),
@@ -2707,28 +2709,24 @@
     result.append("\n");
 }
 
+// This is invoked by SyncEvent callback.
 void AudioFlinger::RecordThread::RecordTrack::handleSyncStartEvent(
         const sp<audioflinger::SyncEvent>& event)
 {
-    if (event == mSyncStartEvent) {
-        ssize_t framesToDrop = 0;
-        sp<ThreadBase> threadBase = mThread.promote();
-        if (threadBase != 0) {
-            // TODO: use actual buffer filling status instead of 2 buffers when info is available
-            // from audio HAL
-            framesToDrop = threadBase->mFrameCount * 2;
-        }
-        mFramesToDrop = framesToDrop;
+    size_t framesToDrop = 0;
+    sp<ThreadBase> threadBase = mThread.promote();
+    if (threadBase != 0) {
+        // TODO: use actual buffer filling status instead of 2 buffers when info is available
+        // from audio HAL
+        framesToDrop = threadBase->mFrameCount * 2;
     }
+
+    mSynchronizedRecordState.onPlaybackFinished(event, framesToDrop);
 }
 
 void AudioFlinger::RecordThread::RecordTrack::clearSyncStartEvent()
 {
-    if (mSyncStartEvent != 0) {
-        mSyncStartEvent->cancel();
-        mSyncStartEvent.clear();
-    }
-    mFramesToDrop = 0;
+    mSynchronizedRecordState.clear();
 }
 
 void AudioFlinger::RecordThread::RecordTrack::updateTrackFrameInfo(
diff --git a/services/audioflinger/timing/SynchronizedRecordState.h b/services/audioflinger/timing/SynchronizedRecordState.h
new file mode 100644
index 0000000..f40d41b
--- /dev/null
+++ b/services/audioflinger/timing/SynchronizedRecordState.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SyncEvent.h"
+
+#pragma push_macro("LOG_TAG")
+#undef LOG_TAG
+#define LOG_TAG "SynchronizedRecordState"
+
+namespace android::audioflinger {
+
+class SynchronizedRecordState {
+public:
+    explicit SynchronizedRecordState(uint32_t sampleRate)
+        : mSampleRate(sampleRate)
+        {}
+
+    void clear() {
+        std::lock_guard lg(mLock);
+        clear_l();
+    }
+
+    // Called by the RecordThread when recording is starting.
+    void startRecording(const sp<SyncEvent>& event) {
+        std::lock_guard lg(mLock);
+        mSyncStartEvent = event;
+        // Sync event can be cancelled by the trigger session if the track is not in a
+        // compatible state in which case we start record immediately
+        if (mSyncStartEvent->isCancelled()) {
+            clear_l();
+        } else {
+            mFramesToDrop = -(ssize_t)
+                ((AudioSystem::kSyncRecordStartTimeOutMs * mSampleRate) / 1000);
+        }
+    }
+
+    // Invoked by SyncEvent callback.
+    void onPlaybackFinished(const sp<SyncEvent>& event, size_t framesToDrop = 1) {
+        std::lock_guard lg(mLock);
+        if (event == mSyncStartEvent) {
+            mFramesToDrop = framesToDrop;  // compute this
+            ALOGV("%s: framesToDrop:%zd", __func__, mFramesToDrop);
+        }
+    }
+
+    // Returns the current FramesToDrop counter
+    //
+    //   if <0 waiting (drop the frames)
+    //   if >0 draining (drop the frames)
+    //    else if ==0 proceed to record.
+    ssize_t updateRecordFrames(size_t frames) {
+        std::lock_guard lg(mLock);
+        if (mFramesToDrop > 0) {
+            // we've been triggered, we count down for start delay
+            ALOGV("%s: trigger countdown %zd by %zu frames", __func__, mFramesToDrop, frames);
+            mFramesToDrop -= (ssize_t)frames;
+            if (mFramesToDrop <= 0) clear_l();
+        } else if (mFramesToDrop < 0) {
+            // we're waiting to be triggered.
+            // ALOGD("%s: timeout countup %zd with %zu frames", __func__, mFramesToDrop, frames);
+            mFramesToDrop += (ssize_t)frames;
+            if (mFramesToDrop >= 0 || !mSyncStartEvent || mSyncStartEvent->isCancelled()) {
+                ALOGW("Synced record %s, trigger session %d",
+                        (mFramesToDrop >= 0) ? "timed out" : "cancelled",
+                        (mSyncStartEvent) ? mSyncStartEvent->triggerSession()
+                                          : AUDIO_SESSION_NONE);
+                 clear_l();
+            }
+        }
+        return mFramesToDrop;
+    }
+
+private:
+    const uint32_t mSampleRate;
+
+    std::mutex mLock;
+    // number of captured frames to drop after the start sync event has been received.
+    // when < 0, maximum frames to drop before starting capture even if sync event is
+    // not received
+    ssize_t mFramesToDrop GUARDED_BY(mLock) = 0;
+
+    // sync event triggering actual audio capture. Frames read before this event will
+    // be dropped and therefore not read by the application.
+    sp<SyncEvent> mSyncStartEvent GUARDED_BY(mLock);
+
+    void clear_l() REQUIRES(mLock) {
+        if (mSyncStartEvent) {
+            mSyncStartEvent->cancel();
+            mSyncStartEvent.clear();
+        }
+        mFramesToDrop = 0;
+    }
+};
+
+} // namespace android::audioflinger
+
+#pragma pop_macro("LOG_TAG")
diff --git a/services/audioflinger/timing/tests/Android.bp b/services/audioflinger/timing/tests/Android.bp
index c360799..d1e5563 100644
--- a/services/audioflinger/timing/tests/Android.bp
+++ b/services/audioflinger/timing/tests/Android.bp
@@ -51,4 +51,29 @@
         "-Werror",
         "-Wextra",
     ],
-}
\ No newline at end of file
+}
+
+cc_test {
+     name: "synchronizedrecordstate_tests",
+
+     host_supported: true,
+
+     srcs: [
+         "synchronizedrecordstate_tests.cpp"
+     ],
+
+     header_libs: [
+         "libaudioclient_headers",
+     ],
+
+     static_libs: [
+         "liblog",
+         "libutils", // RefBase
+     ],
+
+     cflags: [
+         "-Wall",
+         "-Werror",
+         "-Wextra",
+     ],
+ }
\ No newline at end of file
diff --git a/services/audioflinger/timing/tests/synchronizedrecordstate_tests.cpp b/services/audioflinger/timing/tests/synchronizedrecordstate_tests.cpp
new file mode 100644
index 0000000..ee5d269
--- /dev/null
+++ b/services/audioflinger/timing/tests/synchronizedrecordstate_tests.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "synchronizedrecordstate_tests"
+
+#include "../SynchronizedRecordState.h"
+
+#include <gtest/gtest.h>
+
+using namespace android;
+using namespace android::audioflinger;
+
+namespace {
+
+TEST(SynchronizedRecordStateTests, Basic) {
+    struct Cookie : public RefBase {};
+
+    // These variables are set by trigger().
+    bool triggered = false;
+    wp<SyncEvent> param;
+
+    constexpr auto type = AudioSystem::SYNC_EVENT_PRESENTATION_COMPLETE;
+    constexpr auto triggerSession = audio_session_t(10);
+    constexpr auto listenerSession = audio_session_t(11);
+    const SyncEventCallback callback =
+            [&](const wp<SyncEvent>& event) {
+                triggered = true;
+                param = event;
+            };
+    const auto cookie = sp<Cookie>::make();
+
+    // Check timeout.
+    SynchronizedRecordState recordState(48000 /* sampleRate */);
+    auto syncEvent = sp<SyncEvent>::make(
+            type,
+            triggerSession,
+            listenerSession,
+            callback,
+            cookie);
+    recordState.startRecording(syncEvent);
+    recordState.updateRecordFrames(2);
+    ASSERT_FALSE(triggered);
+    ASSERT_EQ(0, recordState.updateRecordFrames(1'000'000'000));
+    ASSERT_FALSE(triggered);
+    ASSERT_TRUE(syncEvent->isCancelled());
+
+    // Check count down after track is complete.
+    syncEvent = sp<SyncEvent>::make(
+                type,
+                triggerSession,
+                listenerSession,
+                callback,
+                cookie);
+    recordState.startRecording(syncEvent);
+    recordState.onPlaybackFinished(syncEvent, 10);
+    ASSERT_EQ(1, recordState.updateRecordFrames(9));
+    ASSERT_FALSE(triggered);
+    ASSERT_EQ(0, recordState.updateRecordFrames(2));
+    ASSERT_FALSE(triggered);
+    ASSERT_TRUE(syncEvent->isCancelled());
+}
+
+}