Add vhal replay for car data

Add 2 buttons to the car data page to:
  Import Json file
  Send emulator event according to the json

Todo: Improve UI
  Add Pause
  Add json format check when format is decided

Hide the feature behand CarVhalReplay flag

Test: emulator -feature CarVhalReplay
Bug: 150640811
Change-Id: Id38f389ebf9659513651fe66c42048cef3837b31
diff --git a/android/android-emu/android/featurecontrol/FeatureControlDefHost.h b/android/android-emu/android/featurecontrol/FeatureControlDefHost.h
index 2edb265..f4e7c5a 100644
--- a/android/android-emu/android/featurecontrol/FeatureControlDefHost.h
+++ b/android/android-emu/android/featurecontrol/FeatureControlDefHost.h
@@ -55,3 +55,4 @@
 FEATURE_CONTROL_ITEM(CarVHalTable)
 FEATURE_CONTROL_ITEM(VulkanSnapshots)
 FEATURE_CONTROL_ITEM(DynamicMediaProfile)
+FEATURE_CONTROL_ITEM(CarVhalReplay)
diff --git a/android/android-emu/android/featurecontrol/proto/emulator_features.proto b/android/android-emu/android/featurecontrol/proto/emulator_features.proto
index 319b689..a50e5d1 100644
--- a/android/android-emu/android/featurecontrol/proto/emulator_features.proto
+++ b/android/android-emu/android/featurecontrol/proto/emulator_features.proto
@@ -89,4 +89,6 @@
     Mac80211hwsimUserspaceManaged = 50;
 
     HasSharedSlotsHostMemoryAllocator = 51;
+
+    CarVhalReplay = 52;
 }
diff --git a/android/android-emu/android/metrics/metrics.cpp b/android/android-emu/android/metrics/metrics.cpp
index 337a9b3..20f7fb3 100644
--- a/android/android-emu/android/metrics/metrics.cpp
+++ b/android/android-emu/android/metrics/metrics.cpp
@@ -435,6 +435,8 @@
             return android_studio::EmulatorFeatureFlagState::MAC80211HWSIM_USERSPACE_MANAGED;
         case android::featurecontrol::HasSharedSlotsHostMemoryAllocator:
             return android_studio::EmulatorFeatureFlagState::HAS_SHARED_SLOTS_HOST_MEMORY_ALLOCATOR;
+        case android::featurecontrol::CarVhalReplay:
+            return android_studio::EmulatorFeatureFlagState::CAR_VHAL_REPLAY;
     }
     return android_studio::EmulatorFeatureFlagState::EMULATOR_FEATURE_FLAG_UNSPECIFIED;
 }
diff --git a/android/android-emu/android/metrics/proto/studio_stats.proto b/android/android-emu/android/metrics/proto/studio_stats.proto
index 046a6e4..ae3c0db 100755
--- a/android/android-emu/android/metrics/proto/studio_stats.proto
+++ b/android/android-emu/android/metrics/proto/studio_stats.proto
@@ -1426,7 +1426,8 @@
     VIRTIO_GPU_NEXT = 55;
     MAC80211HWSIM_USERSPACE_MANAGED = 56;
     HAS_SHARED_SLOTS_HOST_MEMORY_ALLOCATOR = 57;
-    // Next tag: 58
+    CAR_VHAL_REPLAY = 58;
+    // Next tag: 59
   }
   // Which features were enabled by default or through the server-side config.
   repeated EmulatorFeatureFlag attempted_enabled_feature_flags = 1;
diff --git a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.cpp b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.cpp
index 61137a6..6929c21 100644
--- a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.cpp
+++ b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.cpp
@@ -10,17 +10,27 @@
 // GNU General Public License for more details.
 #include "android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h"
 
-#include <stdint.h>                                      // for int32_t
-#include <QCheckBox>                                     // for QCheckBox
-#include <QComboBox>                                     // for QComboBox
-#include <QLabel>                                        // for QLabel
-#include <QSlider>                                       // for QSlider
-#include <utility>                                       // for move
+#include <stdint.h>  // for int32_t
 
-#include "android/emulation/proto/VehicleHalProto.pb.h"  // for EmulatorMessage
-#include "android/utils/debug.h"                         // for VERBOSE_PRINT
-#include "ui_car-sensor-data.h"                          // for CarSensorData
-#include "vehicle_constants_generated.h"                 // for VehicleIgnit...
+#include <QByteArray>     // for QByteArray
+#include <QCheckBox>      // for QCheckBox
+#include <QComboBox>      // for QComboBox
+#include <QFileDialog>    // for QFileDialog
+#include <QJsonArray>     // for QJsonArray
+#include <QJsonDocument>  // for QJsonDocument
+#include <QJsonObject>    // for QJsonObject
+#include <QJsonValue>     // for QJsonValue
+#include <QLabel>         // for QLabel
+#include <QSlider>        // for QSlider
+#include <QTextStream>    // for QSlider
+#include <utility>        // for move
+
+#include "android/base/Log.h"
+#include "android/featurecontrol/feature_control.h"
+#include "android/skin/qt/error-dialog.h"  // for showErrorDialog
+#include "android/utils/debug.h"           // for VERBOSE_PRINT
+#include "ui_car-sensor-data.h"            // for CarSensorData
+#include "vehicle_constants_generated.h"   // for VehicleIgnit...
 
 class QWidget;
 
@@ -31,14 +41,26 @@
 using emulator::EmulatorMessage;
 using emulator::MsgType;
 using emulator::Status;
-using emulator::VehicleProperty;
-using emulator::VehiclePropValue;
+using emulator::TimedEmulatorMessages;
 using emulator::VehicleGear;
 using emulator::VehicleIgnitionState;
+using emulator::VehicleProperty;
+using emulator::VehiclePropValue;
+using emulator::VhalEventLoaderThread;
+
+static constexpr int64_t VHAL_REPLAY_INTERVAL = 1000;
 
 CarSensorData::CarSensorData(QWidget* parent)
     : QWidget(parent), mUi(new Ui::CarSensorData) {
     mUi->setupUi(this);
+
+    if (!feature_is_enabled(kFeature_CarVhalReplay)) {
+        mUi->button_loadrecord->setVisible(false);
+        mUi->button_playrecord->setVisible(false);
+        QObject::connect(&mTimer, &QTimer::timeout, this,
+                     &CarSensorData::VhalTimeout);
+        prepareVhalLoader();
+    }
 }
 
 static const enum VehicleGear sComboBoxGearValues[] = {
@@ -57,6 +79,21 @@
     return emulatorMsg;
 }
 
+void CarSensorData::VhalTimeout() {
+    if (mTimedEmulatorMessages.getStatus() != TimedEmulatorMessages::START) {
+        mTimer.stop();
+        return;
+    }
+    std::vector<emulator::EmulatorMessage> events =
+            mTimedEmulatorMessages.getEvents(VHAL_REPLAY_INTERVAL);
+    int realIndex = mTimedEmulatorMessages.getCurrentIndex() - events.size();
+    for (auto event : events) {
+        string log = "Send event from json index: " + std::to_string(realIndex);
+        realIndex++;
+        mSendEmulatorMsg(event, log);
+    }
+}
+
 void CarSensorData::sendGearChangeMsg(const int gear, const string& gearName) {
     // TODO: Grey out the buttons when callback is not set or vehicle hal is
     // not connected.
@@ -148,6 +185,40 @@
                       mUi->comboBox_gear->currentText().toStdString());
 }
 
+void CarSensorData::on_button_loadrecord_clicked() {
+    prepareVhalLoader();
+
+    QString fileName = QFileDialog::getOpenFileName(
+            this, tr("Open Json File"), ".", tr("Json files (*.json)"));
+
+    if (fileName.isNull())
+        return;
+    parseEventsFromJsonFile(fileName);
+}
+
+void CarSensorData::prepareVhalLoader() {
+    mVhalEventLoader.reset(VhalEventLoaderThread::newInstance());
+
+    connect(mVhalEventLoader.get(), &VhalEventLoaderThread::started, this,
+            &CarSensorData::vhalEventThreadStarted);
+
+    connect(mVhalEventLoader.get(), &VhalEventLoaderThread::loadingFinished,
+            this, &CarSensorData::startupVhalEventThreadFinished);
+
+    // Make sure new_instance gets cleaned up after the thread exits.
+    connect(mVhalEventLoader.get(), &VhalEventLoaderThread::finished,
+            mVhalEventLoader.get(), &QObject::deleteLater);
+}
+
+void CarSensorData::on_button_playrecord_clicked() {
+    mTimedEmulatorMessages.setStatus(TimedEmulatorMessages::START);
+    mTimer.start(VHAL_REPLAY_INTERVAL);
+}
+
+void CarSensorData::parseEventsFromJsonFile(QString jsonPath) {
+    mVhalEventLoader->loadVhalEventFromFile(jsonPath, &mTimedEmulatorMessages);
+}
+
 void CarSensorData::processMsg(emulator::EmulatorMessage emulatorMsg) {
     if (emulatorMsg.prop_size() == 0 && emulatorMsg.value_size() == 0) {
         return;
@@ -226,3 +297,226 @@
     }
     return len - 1;
 }
+
+void CarSensorData::vhalEventThreadStarted() {
+    // Prevent the user from initiating a load json while another load is
+    // already in progress
+    mUi->button_loadrecord->setEnabled(false);
+    mUi->button_playrecord->setEnabled(false);
+
+    mNowLoadingVhalEvent = true;
+}
+
+void CarSensorData::startupVhalEventThreadFinished(QString file_name,
+                                                   bool ok,
+                                                   QString error_message) {
+    // on startup, we silently ignore the previously remebered event data file
+    // being missing or malformed.
+    finishVhalEventLoading(file_name, ok, error_message, true);
+}
+
+void CarSensorData::vhalEventThreadFinished(QString file_name,
+                                            bool ok,
+                                            QString error_message) {
+    // on startup, we silently ignore the previously remebered event data file
+    // being missing or malformed.
+    finishVhalEventLoading(file_name, ok, error_message, false);
+}
+
+void CarSensorData::finishVhalEventLoading(const QString& file_name,
+                                           bool ok,
+                                           const QString& error_message,
+                                           bool ignore_error) {
+    mVhalEventLoader.reset();
+    updateControlsAfterLoading();
+
+    if (!ok) {
+        if (!ignore_error) {
+            showErrorDialog(error_message, tr("Vhal EVENT Parser"));
+        }
+        return;
+    }
+}
+
+void CarSensorData::updateControlsAfterLoading() {
+    mUi->button_loadrecord->setEnabled(true);
+    mUi->button_playrecord->setEnabled(true);
+
+    mNowLoadingVhalEvent = false;
+}
+
+void VhalEventLoaderThread::loadVhalEventFromFile(
+        const QString& file_name,
+        TimedEmulatorMessages* events) {
+    mFileName = file_name;
+    mTimedEmulatorMessages = events;
+    start();
+}
+
+void VhalEventLoaderThread::run() {
+    if (mFileName.isEmpty() || mTimedEmulatorMessages == nullptr) {
+        emit(loadingFinished(mFileName, false, tr("No file to load")));
+        return;
+    }
+    bool ok = false;
+    std::string err_str;
+
+    QFileInfo file_info(mFileName);
+    mTimedEmulatorMessages->clear();
+    auto suffix = file_info.suffix().toLower();
+    if (suffix == "json") {
+        ok = parseJsonFile(mFileName.toStdString().c_str(),
+                           mTimedEmulatorMessages);
+    } else {
+        err_str = tr("Unknown file type").toStdString();
+    }
+
+    auto err_qstring = QString::fromStdString(err_str);
+    emit(loadingFinished(mFileName, ok, err_qstring));
+}
+
+bool VhalEventLoaderThread::parseJsonFile(
+        const char* filePath,
+        TimedEmulatorMessages* timedEmulatorMessages) {
+    QString jsonString;
+    if (filePath) {
+        jsonString = readJsonStringFromFile(filePath);
+    }
+
+    QJsonDocument eventDoc = QJsonDocument::fromJson(jsonString.toUtf8());
+    if (eventDoc.isNull()) {
+        return false;
+    } else {
+        return loadEmulatorEvents(eventDoc, timedEmulatorMessages);
+    }
+    return false;
+}
+
+bool VhalEventLoaderThread::loadEmulatorEvents(
+        const QJsonDocument& eventDoc,
+        TimedEmulatorMessages* timedEmulatorMessages) {
+    if (eventDoc.isNull()) {
+        return false;
+    }
+    QJsonObject eventsJson = eventDoc.object();
+    QJsonArray eventArray = eventsJson.value("events").toArray();
+
+    for (int eventIdx = 0; eventIdx < eventArray.size(); eventIdx++) {
+        QJsonObject eventObject = eventArray.at(eventIdx).toObject();
+        QJsonDocument Doc(eventObject);
+        QByteArray ba = Doc.toJson();
+        QJsonObject sensorrecord =
+                eventObject.value("sensor_records").toObject();
+        QJsonObject carPropertyValues =
+                sensorrecord.value("car_property_values").toObject();
+        int propID = carPropertyValues.value("key").toInt();
+
+        EmulatorMessage emulatorMsg = makeSetPropMsg();
+        VehiclePropValue* value = emulatorMsg.add_value();
+        value->set_prop(static_cast<int32_t>(propID));
+
+        int prop = carPropertyValues.value("key").toInt();
+        int areaId = carPropertyValues.value("value")
+                             .toObject()
+                             .value("area_id")
+                             .toInt();
+        QJsonObject propertyValue = carPropertyValues.value("value").toObject();
+        int type = 0;
+        if (propertyValue.contains("int32_values")) {
+            value->add_int32_values(
+                    propertyValue.value("int32_values").toInt());
+        } else if (propertyValue.contains("float_values")) {
+            value->add_float_values(
+                    (float)propertyValue.value("float_values").toDouble());
+        }
+
+        char* error = nullptr;
+        timedEmulatorMessages->addEvents(
+                strtol(sensorrecord.value("timestamp_ns")
+                               .toString()
+                               .toStdString()
+                               .c_str(),
+                       &error, 0),
+                emulatorMsg);
+    }
+
+    return true;
+}
+
+QString VhalEventLoaderThread::readJsonStringFromFile(const char* filePath) {
+    QString fullContents;
+    if (filePath) {
+        QFile jsonFile(filePath);
+        if (jsonFile.open(QFile::ReadOnly | QFile::Text)) {
+            QTextStream jsonStream(&jsonFile);
+            fullContents = jsonStream.readAll();
+            jsonFile.close();
+        }
+    }
+    return fullContents;
+}
+
+VhalEventLoaderThread* VhalEventLoaderThread::newInstance() {
+    VhalEventLoaderThread* new_instance = new VhalEventLoaderThread();
+    return new_instance;
+}
+
+std::vector<emulator::EmulatorMessage> TimedEmulatorMessages::getEvents(
+        int64_t interval) {
+    std::vector<emulator::EmulatorMessage> res;
+
+    if (mTimestampes.size() != mEmulatorMessages.size()) {
+        clear();
+        return res;
+    }
+    if (mCurrIndex == mEmulatorMessages.size()) {
+        mCurrIndex = 0;
+        mBaseTimeStamp = -1;
+        mStatus = STOP;
+        return res;
+    }
+
+    int64_t nextTimeStamp = mBaseTimeStamp + (int64_t)interval;
+
+    for (; mCurrIndex < mEmulatorMessages.size(); mCurrIndex++) {
+        int64_t timestamp = mTimestampes.at(mCurrIndex);
+        if (timestamp >= mBaseTimeStamp && timestamp < nextTimeStamp) {
+            res.push_back(mEmulatorMessages.at(mCurrIndex));
+        } else {
+            break;
+        }
+    }
+    mBaseTimeStamp = nextTimeStamp;
+    return res;
+}
+
+void TimedEmulatorMessages::addEvents(int64_t timestamp,
+                                      emulator::EmulatorMessage& msg) {
+    if (mBaseTimeStamp == -1) {
+        mBaseTimeStamp = timestamp;
+    }
+    if (mTimestampes.size() != mEmulatorMessages.size()) {
+        return;
+    }
+    mEmulatorMessages.push_back(msg);
+    mTimestampes.push_back(timestamp);
+}
+
+void TimedEmulatorMessages::clear() {
+    mEmulatorMessages.clear();
+    mTimestampes.clear();
+    mCurrIndex = 0;
+    mBaseTimeStamp = -1;
+}
+
+TimedEmulatorMessages::PlayStatus TimedEmulatorMessages::getStatus() {
+    return mStatus;
+}
+
+void TimedEmulatorMessages::setStatus(PlayStatus status) {
+    mStatus = status;
+}
+
+int TimedEmulatorMessages::getCurrentIndex() {
+    return mCurrIndex;
+}
\ No newline at end of file
diff --git a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h
index e89cd43..5734f74 100644
--- a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h
+++ b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h
@@ -10,19 +10,74 @@
 // GNU General Public License for more details.
 #pragma once
 
-#include <qobjectdefs.h>         // for Q_OBJECT, slots
-#include <QString>               // for QString
-#include <QWidget>               // for QWidget
-#include <functional>            // for function
-#include <memory>                // for unique_ptr
-#include <string>                // for string
+// TODO: (b/120444474) rename ERROR_INVALID_OPERATION & remove this undef
+#undef ERROR_INVALID_OPERATION
 
-#include "ui_car-sensor-data.h"  // for CarSensorData
+#include <qobjectdefs.h>  // for Q_OBJECT, slots
+
+#include <QString>     // for QString
+#include <QThread>     // for QThread
+#include <QTimer>      // for QTimer
+#include <QWidget>     // for QWidget
+#include <functional>  // for function
+#include <memory>      // for unique_ptr
+#include <string>      // for string
+
+#include "android/emulation/proto/VehicleHalProto.pb.h"  // for EmulatorMessage
+#include "ui_car-sensor-data.h"                          // for CarSensorData
 
 class QObject;
 class QWidget;
 
 namespace emulator {
+
+class TimedEmulatorMessages {
+public:
+    enum PlayStatus { STOP, START, PAUSE };
+
+    std::vector<emulator::EmulatorMessage> getEvents(int64_t interval);
+    void addEvents(int64_t timestamp, emulator::EmulatorMessage& msg);
+    void clear();
+    PlayStatus getStatus();
+    void setStatus(PlayStatus status);
+    int getCurrentIndex();
+
+private:
+    std::vector<emulator::EmulatorMessage> mEmulatorMessages;
+    std::vector<int64_t> mTimestampes;
+    int mCurrIndex = 0;
+    int64_t mBaseTimeStamp = -1;
+    PlayStatus mStatus;
+};
+
+class VhalEventLoaderThread : public QThread {
+    Q_OBJECT
+public:
+    // Loads Vhal from a json file specified
+    // by file_name into the EmulatorMessage array
+    void loadVhalEventFromFile(const QString& file_name,
+                               emulator::TimedEmulatorMessages* events);
+
+    static VhalEventLoaderThread* newInstance();
+
+signals:
+    void loadingFinished(QString file_name, bool ok, QString error);
+
+protected:
+    // Reimplemented to load the file into the given fixes array.
+    void run() override;
+
+private:
+    VhalEventLoaderThread() = default;
+    QString mFileName;
+    emulator::TimedEmulatorMessages* mTimedEmulatorMessages = nullptr;
+    bool parseJsonFile(const char* filePath,
+                       emulator::TimedEmulatorMessages* emulatorMessages);
+    QString readJsonStringFromFile(const char* filePath);
+    bool loadEmulatorEvents(const QJsonDocument& eventDoc,
+                            emulator::TimedEmulatorMessages* emulatorMessages);
+};
+
 class EmulatorMessage;
 }
 class CarSensorData : public QWidget {
@@ -42,6 +97,12 @@
     void on_checkBox_night_toggled();
     void on_checkBox_park_toggled();
     void on_checkBox_fuel_low_toggled();
+    void on_button_loadrecord_clicked();
+    void on_button_playrecord_clicked();
+    void vhalEventThreadStarted();
+    void startupVhalEventThreadFinished(QString file_name,
+                                        bool ok,
+                                        QString error);
 
 private:
     std::unique_ptr<Ui::CarSensorData> mUi;
@@ -57,4 +118,22 @@
                                const std::string& ignitionName);
     float getSpeedMetersPerSecond(int speed, int unitIndex);
     int getIndexFromVehicleGear(int gear);
-};
+    void parseEventsFromJsonFile(QString jsonPath);
+
+    // Vhal replay
+    std::unique_ptr<emulator::VhalEventLoaderThread> mVhalEventLoader;
+    emulator::TimedEmulatorMessages mTimedEmulatorMessages;
+    bool mNowLoadingVhalEvent = false;
+    QTimer mTimer;
+
+    void VhalTimeout();
+    void vhalEventThreadFinished(QString file_name,
+                                 bool ok,
+                                 QString error_message);
+    void updateControlsAfterLoading();
+    void finishVhalEventLoading(const QString& file_name,
+                                bool ok,
+                                const QString& error_message,
+                                bool ignore_error);
+    void prepareVhalLoader();
+};
\ No newline at end of file
diff --git a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.ui b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.ui
index eeea38a..a68b4e2 100644
--- a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.ui
+++ b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.ui
@@ -6,7 +6,7 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>621</width>
+    <width>460</width>
     <height>456</height>
    </rect>
   </property>
@@ -399,6 +399,32 @@
     <string>Title</string>
    </property>
   </widget>
+  <widget class="QPushButton" name="button_loadrecord">
+   <property name="geometry">
+    <rect>
+     <x>20</x>
+     <y>210</y>
+     <width>95</width>
+     <height>26</height>
+    </rect>
+   </property>
+   <property name="text">
+    <string>LOAD RECORD</string>
+   </property>
+  </widget>
+  <widget class="QPushButton" name="button_playrecord">
+   <property name="geometry">
+    <rect>
+     <x>20</x>
+     <y>250</y>
+     <width>95</width>
+     <height>26</height>
+    </rect>
+   </property>
+   <property name="text">
+    <string>PLAY RECORD</string>
+   </property>
+  </widget>
  </widget>
  <resources/>
  <connections/>
diff --git a/android/data/advancedFeatures.ini b/android/data/advancedFeatures.ini
index 96e33f0c..3b0e92c 100644
--- a/android/data/advancedFeatures.ini
+++ b/android/data/advancedFeatures.ini
@@ -304,3 +304,7 @@
 # HasSharedSlotsHostMemoryAllocator---------------------------------------------
 # Host supports AddressSpaceSharedSlotsHostMemoryAllocatorContext
 HasSharedSlotsHostMemoryAllocator = on
+
+# CarVHalReplay--------------------------------------------------------------
+# if enabled, Car Vhal Load and Play button will show in extended window -> CarData
+CarVhalReplay = off
\ No newline at end of file
diff --git a/android/data/advancedFeaturesCanary.ini b/android/data/advancedFeaturesCanary.ini
index 2ca7c84..096062d 100644
--- a/android/data/advancedFeaturesCanary.ini
+++ b/android/data/advancedFeaturesCanary.ini
@@ -309,3 +309,7 @@
 # HasSharedSlotsHostMemoryAllocator---------------------------------------------
 # Host supports AddressSpaceSharedSlotsHostMemoryAllocatorContext
 HasSharedSlotsHostMemoryAllocator = on
+
+# CarVHalReplay--------------------------------------------------------------
+# if enabled, Car Vhal Load and Play button will show in extended window -> CarData
+CarVhalReplay = off
\ No newline at end of file