Add sthal_cli_3

This is a command-line HAL mock, which registers as an AIDL HAL.

Change-Id: Idab4c6298bbdd7dafc360c50ac519efb7066cd0e
diff --git a/soundtrigger/3/cli/Android.bp b/soundtrigger/3/cli/Android.bp
new file mode 100644
index 0000000..880903a
--- /dev/null
+++ b/soundtrigger/3/cli/Android.bp
@@ -0,0 +1,8 @@
+java_binary {
+    name: "sthal_cli_3",
+    wrapper: "sthal_cli_3",
+    srcs: ["java/**/*.java"],
+    static_libs: [
+        "android.hardware.soundtrigger3-V1-java",
+    ],
+}
diff --git a/soundtrigger/3/cli/OWNERS b/soundtrigger/3/cli/OWNERS
new file mode 100644
index 0000000..e21b66e
--- /dev/null
+++ b/soundtrigger/3/cli/OWNERS
@@ -0,0 +1 @@
+include /media/java/android/media/soundtrigger_middleware/OWNERS
diff --git a/soundtrigger/3/cli/java/android/hardware/soundtrigger3/cli/SthalCli.java b/soundtrigger/3/cli/java/android/hardware/soundtrigger3/cli/SthalCli.java
new file mode 100644
index 0000000..d3e1aa7
--- /dev/null
+++ b/soundtrigger/3/cli/java/android/hardware/soundtrigger3/cli/SthalCli.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright 2021 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.
+ */
+package android.hardware.soundtrigger3.cli;
+
+import android.annotation.NonNull;
+import android.hardware.soundtrigger3.ISoundTriggerHw;
+import android.hardware.soundtrigger3.ISoundTriggerHwCallback;
+import android.hardware.soundtrigger3.ISoundTriggerHwGlobalCallback;
+import android.media.audio.common.AudioConfig;
+import android.media.soundtrigger.ConfidenceLevel;
+import android.media.soundtrigger.ModelParameterRange;
+import android.media.soundtrigger.PhraseRecognitionEvent;
+import android.media.soundtrigger.PhraseRecognitionExtra;
+import android.media.soundtrigger.PhraseSoundModel;
+import android.media.soundtrigger.Properties;
+import android.media.soundtrigger.RecognitionConfig;
+import android.media.soundtrigger.RecognitionEvent;
+import android.media.soundtrigger.RecognitionMode;
+import android.media.soundtrigger.SoundModel;
+import android.media.soundtrigger.SoundModelType;
+import android.os.HwBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+
+import java.util.Scanner;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * This is a quick-and-dirty sound trigger HAL console mock.
+ *
+ * It would only work on userdebug builds.
+ *
+ * When this app is started, it will initially:
+ * - Register a ISoundTriggerHw HAL with an instance name "mock".
+ * - Set a sysprop that tells SoundTriggerMiddlewareService to try to connect to the mock instance
+ * rather than the default one.
+ * - Reboot the real (default) HAL.
+ *
+ * In response to that, SoundTriggerMiddlewareService is going to connect to the mock HAL and resume
+ * normal operation.
+ *
+ * Our mock HAL will print to stdout every call it receives as well as expose a basic set of
+ * operations for sending event callbacks to the client. This allows us to simulate the frameworks
+ * behavior in response to different HAL behaviors.
+ */
+public class SthalCli {
+    private static SoundTriggerImpl mService;
+    private static final Scanner scanner = new Scanner(System.in);
+
+    public static void main(String[] args) {
+        try {
+            printUsage();
+
+            System.out.println("Registering mock STHAL");
+            mService = new SoundTriggerImpl();
+            // This allows us to register the service, even if it is not declared in the manifest.
+            mService.forceDowngradeToSystemStability();
+            ServiceManager.addService(ISoundTriggerHw.class.getCanonicalName() + "/mock", mService);
+
+            System.out.println("Rebooting STHAL");
+            SystemProperties.set("debug.soundtrigger_middleware.use_mock_hal", "3");
+            SystemProperties.set("sys.audio.restart.hal", "1");
+
+            while (processCommand()) ;
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            cleanup();
+        }
+    }
+
+    private static void cleanup() {
+        System.out.println("Cleaning up.");
+        SystemProperties.set("debug.soundtrigger_middleware.use_mock_hal", null);
+        HwBinder.setTrebleTestingOverride(false);
+    }
+
+    private static boolean processCommand() {
+        String line = scanner.nextLine();
+        String[] tokens = line.split("\\s+");
+        if (tokens.length < 1) {
+            return false;
+        }
+        switch (tokens[0]) {
+            case "q":
+                return false;
+
+            case "a":
+                mService.sendOnResourcesAvailable();
+                return true;
+
+            case "u":
+                mService.sendModelUnloaded(Integer.parseInt(tokens[1]));
+                return true;
+
+            case "r":
+                mService.sendRecognitionEvent(Integer.parseInt(tokens[1]),
+                        Integer.parseInt(tokens[2]));
+                return true;
+
+            case "p":
+                mService.sendPhraseRecognitionEvent(Integer.parseInt(tokens[1]),
+                        Integer.parseInt(tokens[2]));
+                return true;
+
+            case "d":
+                mService.dumpModels();
+                return true;
+
+            default:
+                printUsage();
+                return true;
+        }
+    }
+
+    private static void printUsage() {
+        System.out.print(
+                "Sound Trigger HAL v3 mock\n"
+                + "Available commands:\n"
+                + "h - help\n"
+                + "q - quit\n"
+                + "a - send onResourcesAvailable event\n"
+                + "u <model> - send modelUnloaded event\n"
+                + "r <model> <status> - send recognitionEvent\n"
+                + "p <model> <status> - send phraseRecognitionEvent\n"
+                + "d - dump models\n");
+    }
+
+    private static class SoundTriggerImpl extends ISoundTriggerHw.Stub {
+        static class Model {
+            final ISoundTriggerHwCallback callback;
+            final SoundModel model;
+            final PhraseSoundModel phraseModel;
+            public RecognitionConfig config = null;
+
+            Model(ISoundTriggerHwCallback callback, SoundModel model) {
+                this.callback = callback;
+                this.model = model;
+                this.phraseModel = null;
+            }
+
+            Model(ISoundTriggerHwCallback callback, PhraseSoundModel model) {
+                this.callback = callback;
+                this.model = null;
+                this.phraseModel = model;
+            }
+        }
+
+        private ISoundTriggerHwGlobalCallback mGlobalCallback;
+        private final ConcurrentMap<Integer, Model> mLoadedModels = new ConcurrentHashMap<>();
+        private int mHandleCounter = 1;
+
+        public void dumpModels() {
+            mLoadedModels.forEach((handle, model) -> {
+                System.out.println("+++ Model " + handle);
+                System.out.println("    config = " + model.config);
+                RecognitionConfig recognitionConfig = model.config;
+                if (recognitionConfig != null) {
+                    System.out.println("    ACTIVE recognitionConfig = " + recognitionConfig);
+                } else {
+                    System.out.println("    INACTIVE");
+                }
+            });
+        }
+
+        public void sendOnResourcesAvailable() {
+            if (mGlobalCallback != null) {
+                try {
+                    mGlobalCallback.onResourcesAvailable();
+                } catch (RemoteException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        public void sendRecognitionEvent(int modelHandle, int status) {
+            Model model = mLoadedModels.get(modelHandle);
+            if (model != null && model.config != null) {
+                RecognitionEvent event = new RecognitionEvent();
+                event.type = SoundModelType.GENERIC;
+                event.status = status;
+                event.captureAvailable = true;
+                event.audioConfig.channelMask = 16;
+                event.audioConfig.format = 1;
+                event.audioConfig.sampleRateHz = 16000;
+                try {
+                    model.callback.recognitionCallback(modelHandle, event);
+                } catch (RemoteException e) {
+                    e.printStackTrace();
+                }
+                model.config = null;
+            }
+        }
+
+        public void sendPhraseRecognitionEvent(int modelHandle, int status) {
+            Model model = mLoadedModels.get(modelHandle);
+            if (model != null && model.config != null) {
+                PhraseRecognitionEvent event = new PhraseRecognitionEvent();
+                event.common = new RecognitionEvent();
+                event.common.type = SoundModelType.KEYPHRASE;
+                event.common.status = status;
+                event.common.captureAvailable = true;
+                event.common.audioConfig = new AudioConfig();
+                event.common.audioConfig.channelMask = 16;
+                event.common.audioConfig.format = 1;
+                event.common.audioConfig.sampleRateHz = 16000;
+                if (model.phraseModel.phrases.length > 0) {
+                    PhraseRecognitionExtra extra = new PhraseRecognitionExtra();
+                    extra.id = model.phraseModel.phrases[0].id;
+                    extra.confidenceLevel = 100;
+                    extra.recognitionModes = model.phraseModel.phrases[0].recognitionModes;
+                    extra.levels = new ConfidenceLevel[0];
+                    event.phraseExtras = new PhraseRecognitionExtra[]{extra};
+                } else {
+                    event.phraseExtras = new PhraseRecognitionExtra[0];
+                }
+                try {
+                    model.callback.phraseRecognitionCallback(modelHandle, event);
+                } catch (RemoteException e) {
+                    e.printStackTrace();
+                }
+                model.config = null;
+            }
+        }
+
+        public void sendModelUnloaded(int modelHandle) {
+            Model model = mLoadedModels.remove(modelHandle);
+            if (model != null) {
+                try {
+                    model.callback.modelUnloaded(modelHandle);
+                } catch (RemoteException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        @Override
+        public void registerGlobalCallback(ISoundTriggerHwGlobalCallback callback) {
+            System.out.println("registerGlobalCallback()");
+            mGlobalCallback = callback;
+        }
+
+        @Override
+        public int loadSoundModel(SoundModel soundModel, ISoundTriggerHwCallback callback) {
+            int handle = mHandleCounter++;
+            System.out.printf("loadSoundModel(soundModel=%s) -> %d%n", soundModel, handle);
+            mLoadedModels.put(handle, new Model(callback, soundModel));
+            return handle;
+        }
+
+        @Override
+        public int loadPhraseSoundModel(PhraseSoundModel soundModel,
+                ISoundTriggerHwCallback callback) {
+            int handle = mHandleCounter++;
+            System.out.printf("loadPhraseSoundModel(soundModel=%s) -> %d%n", soundModel, handle);
+            mLoadedModels.put(handle, new Model(callback, soundModel));
+            return handle;
+        }
+
+        @Override
+        public void startRecognition(int modelHandle, int deviceHandle, int ioHandle,
+                RecognitionConfig config) {
+            System.out.printf("startRecognition(modelHandle=%d, deviceHandle=%d, ioHandle=%d)%n",
+                    modelHandle, deviceHandle, ioHandle);
+            Model model = mLoadedModels.get(modelHandle);
+            if (model != null) {
+                model.config = config;
+            }
+        }
+
+        @Override
+        public Properties getProperties() {
+            System.out.println("getProperties()");
+            Properties properties = new Properties();
+            properties.implementor = "Android";
+            properties.description = "Mock STHAL";
+            properties.maxSoundModels = 2;
+            properties.maxKeyPhrases = 1;
+            properties.recognitionModes =
+                    RecognitionMode.VOICE_TRIGGER | RecognitionMode.GENERIC_TRIGGER;
+            return properties;
+        }
+
+        @Override
+        public ModelParameterRange queryParameter(int modelHandle, int modelParam) {
+            System.out.printf("queryParameter(modelHandle=%d, modelParam=%d)%n", modelHandle,
+                    modelParam);
+            return null;
+        }
+
+        @Override
+        public void forceRecognitionEvent(int modelHandle) {
+            System.out.printf("getModelState(modelHandle=%d)%n", modelHandle);
+        }
+
+        @Override
+        public void unloadSoundModel(int modelHandle) {
+            System.out.printf("unloadSoundModel(modelHandle=%d)%n", modelHandle);
+        }
+
+        @Override
+        public void stopRecognition(int modelHandle) {
+            System.out.printf("stopRecognition(modelHandle=%d)%n", modelHandle);
+            Model model = mLoadedModels.get(modelHandle);
+            if (model != null) {
+                model.config = null;
+            }
+        }
+
+        @Override
+        public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+                @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+                @NonNull String[] args) {
+            if (args.length > 0) {
+                switch (args[0]) {
+                    case "reboot":
+                        System.out.println("Received a reboot request. Exiting.");
+                        cleanup();
+                        System.exit(1);
+                }
+            }
+            return 0;
+        }
+
+        @Override
+        public void setParameter(int modelHandle, int modelParam, int value) {
+            throw new IllegalArgumentException();
+        }
+
+        @Override
+        public int getParameter(int modelHandle, int modelParam) {
+            throw new IllegalArgumentException();
+        }
+    }
+}
diff --git a/soundtrigger/3/cli/sthal_cli_3 b/soundtrigger/3/cli/sthal_cli_3
new file mode 100644
index 0000000..f157c50
--- /dev/null
+++ b/soundtrigger/3/cli/sthal_cli_3
@@ -0,0 +1,6 @@
+#!/system/bin/sh
+# Script to start "sthal_cli_3" on the device
+#
+base=/system
+export CLASSPATH=$base/framework/sthal_cli_3.jar
+exec app_process $base/bin android.hardware.soundtrigger3.cli.SthalCli "$@"