diff --git a/samples/powerplay/QuickStart_Automation.md b/samples/powerplay/QuickStart_Automation.md new file mode 100644 index 000000000..ce3532c10 --- /dev/null +++ b/samples/powerplay/QuickStart_Automation.md @@ -0,0 +1,160 @@ +# PowerPlay Automation Guide + +This guide explains how to control the PowerPlay audio player via ADB commands for automated testing of power-efficient audio features (PCM Offload). + +## Quick Start + +```bash +# Build and install +cd samples/powerplay +../../gradlew installDebug + +# Play with PCM Offload enabled +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play \ + --es perf_mode offload + +# Monitor status +adb logcat -s PowerPlay:V +``` + +## Supported ADB Parameters + +| Parameter | Type | Description | Example | +| -------------------- | ------- | ---------------------------------------------------------- | ------------------------------ | +| `command` | string | Action: `play`, `pause`, `stop` | `--es command play` | +| `perf_mode` | string | Performance mode: `none`, `lowlat`, `powersave`, `offload` | `--es perf_mode offload` | +| `song_index` | int | Track index (0-2) | `--ei song_index 1` | +| `volume` | int | Volume percentage (0-100) | `--ei volume 50` | +| `background` | boolean | Move app to background after starting | `--ez background true` | +| `duration_ms` | int | Auto-stop after N milliseconds | `--ei duration_ms 10000` | +| `use_mmap` | boolean | Enable/disable MMAP audio path | `--ez use_mmap false` | +| `buffer_frames` | int | Buffer size in frames (offload only) | `--ei buffer_frames 4096` | +| `toggle_offload` | boolean | Enable offload stress test | `--ez toggle_offload true` | +| `toggle_interval_ms` | int | Toggle interval in ms | `--ei toggle_interval_ms 5000` | + +## Common Test Scenarios + +### Test PCM Offload Mode +```bash +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play \ + --es perf_mode offload +``` + +### Background Playback Test +```bash +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play \ + --es perf_mode offload \ + --ez background true +``` + +### Timed Playback (10 seconds) +```bash +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play \ + --es perf_mode offload \ + --ei duration_ms 10000 +``` + +### Offload Toggle Stress Test +```bash +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play \ + --es perf_mode offload \ + --ez toggle_offload true \ + --ei toggle_interval_ms 3000 +``` + +### Compare Performance Modes +```bash +# Low Latency +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play --es perf_mode lowlat + +# Power Saving (non-offload) +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play --es perf_mode powersave + +# PCM Offload +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play --es perf_mode offload +``` + +### Stop Playback +```bash +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command stop +``` + +## Machine-Readable Status Logs + +The app outputs status to logcat with the `PowerPlay` tag in a parseable format: + +``` +POWERPLAY_STATUS: PLAYING | SONG=0 | OFFLOAD=true | MMAP=true +POWERPLAY_STATUS: PAUSED +POWERPLAY_STATUS: STOPPED | REASON=AUTO_STOP +``` + +### Status Values +| Status | Meaning | +| ----------------- | ------------------------------------- | +| `PLAYING` | Audio playback is active | +| `PAUSED` | Audio playback is paused | +| `STOPPED` | Audio playback has stopped | +| `ERROR` | An error occurred | +| `OFFLOAD_REVOKED` | PCM Offload was revoked by the system | + +### Log Monitoring +```bash +# Real-time monitoring +adb logcat -s PowerPlay:V + +# Filter for status only +adb logcat -s PowerPlay:V | grep "POWERPLAY_STATUS" + +# Save to file +adb logcat -s PowerPlay:V > powerplay_test.log +``` + +## Using the Test Action + +For automation scripts, you can also use the dedicated test action: + +```bash +adb shell am start -a com.google.oboe.samples.powerplay.TEST \ + --es command play \ + --es perf_mode offload +``` + +## Battery Testing + +To measure power consumption with offload vs non-offload: + +```bash +# Reset battery stats +adb shell dumpsys batterystats --reset + +# Run offload test for 5 minutes +adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + --es command play \ + --es perf_mode offload \ + --ez background true \ + --ei duration_ms 300000 + +# Wait for test to complete, then capture stats +adb shell dumpsys batterystats > battery_offload.txt +``` + +## Troubleshooting + +### Offload Not Activating +- Ensure device supports PCM Offload: check `AudioManager.isOffloadedPlaybackSupported()` +- Some devices only support offload with specific sample rates (usually 48kHz) +- Display must be off for some implementations + +### App Doesn't Respond to Commands +- Make sure the app is installed: `adb shell pm list packages | grep powerplay` +- Check if activity is exported: `adb shell dumpsys package com.google.oboe.samples.powerplay` diff --git a/samples/powerplay/scripts/test_powerplay.sh b/samples/powerplay/scripts/test_powerplay.sh new file mode 100755 index 000000000..bf1b8516f --- /dev/null +++ b/samples/powerplay/scripts/test_powerplay.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# PowerPlay Automation Test Script +# Demonstrates a full test cycle: Open -> Play Offload -> Wait -> Close + +set -e + +PACKAGE="com.google.oboe.samples.powerplay" +ACTIVITY="${PACKAGE}/.MainActivity" +LOG_TAG="PowerPlay" +TEST_DURATION_MS=10000 # 10 seconds + +echo "=== PowerPlay Automation Test ===" +echo "" + +# Check device connection +if ! adb devices | grep -q "device$"; then + echo "ERROR: No Android device connected" + exit 1 +fi + +echo "[1/6] Checking if PowerPlay is installed..." +if ! adb shell pm list packages | grep -q "$PACKAGE"; then + echo "ERROR: PowerPlay is not installed. Please run:" + echo " cd samples/powerplay && ../../gradlew installDebug" + exit 1 +fi +echo " ✓ PowerPlay is installed" + +echo "" +echo "[2/6] Force-stopping any existing instance..." +adb shell am force-stop "$PACKAGE" +sleep 1 +echo " ✓ App stopped" + +echo "" +echo "[3/6] Starting playback with PCM Offload mode..." +adb shell am start -n "$ACTIVITY" \ + --es command play \ + --es perf_mode offload \ + --ei duration_ms "$TEST_DURATION_MS" \ + --ei volume 75 + +echo " ✓ Play command sent" + +echo "" +echo "[4/6] Monitoring status (waiting for ${TEST_DURATION_MS}ms)..." +echo "---" + +# Capture and display logs for the test duration +timeout $((TEST_DURATION_MS / 1000 + 2)) adb logcat -s "$LOG_TAG:V" 2>/dev/null | while read -r line; do + if echo "$line" | grep -q "POWERPLAY_STATUS"; then + echo " LOG: $line" + fi +done || true + +echo "---" +echo " ✓ Test duration completed" + +echo "" +echo "[5/6] Verifying playback stopped..." +FINAL_STATUS=$(adb logcat -d -s "$LOG_TAG:V" | grep "POWERPLAY_STATUS" | tail -1) +if echo "$FINAL_STATUS" | grep -q "STOPPED"; then + echo " ✓ Playback stopped correctly" +else + echo " ⚠ Last status: $FINAL_STATUS" +fi + +echo "" +echo "[6/6] Checking offload status from logs..." +if adb logcat -d -s "$LOG_TAG:V" | grep -q "OFFLOAD=true"; then + echo " ✓ PCM Offload was active during playback" +else + echo " ⚠ PCM Offload may not have been active (device might not support it)" +fi + +echo "" +echo "=== Test Complete ===" +echo "" +echo "Full logs saved to: powerplay_test.log" +adb logcat -d -s "$LOG_TAG:V" > powerplay_test.log + +echo "" +echo "To run additional tests:" +echo " # Toggle offload stress test" +echo " adb shell am start -n $ACTIVITY --es command play --es perf_mode offload --ez toggle_offload true" +echo "" +echo " # Background playback" +echo " adb shell am start -n $ACTIVITY --es command play --es perf_mode offload --ez background true" diff --git a/samples/powerplay/src/main/AndroidManifest.xml b/samples/powerplay/src/main/AndroidManifest.xml index 9f629864d..52d6cb64f 100644 --- a/samples/powerplay/src/main/AndroidManifest.xml +++ b/samples/powerplay/src/main/AndroidManifest.xml @@ -14,12 +14,19 @@ + + + + + + diff --git a/samples/powerplay/src/main/cpp/PowerPlayJNI.cpp b/samples/powerplay/src/main/cpp/PowerPlayJNI.cpp index 8139d50c9..f02cd6338 100644 --- a/samples/powerplay/src/main/cpp/PowerPlayJNI.cpp +++ b/samples/powerplay/src/main/cpp/PowerPlayJNI.cpp @@ -275,6 +275,47 @@ Java_com_google_oboe_samples_powerplay_engine_PowerPlayAudioPlayer_getBufferCapa return player.getBufferCapacityInFrames(); } +/** + * Native (JNI) implementation of PowerPlayAudioPlayer.setVolumeNative() + */ +JNIEXPORT void JNICALL +Java_com_google_oboe_samples_powerplay_engine_PowerPlayAudioPlayer_setVolumeNative( + JNIEnv *env, + jobject, + jfloat volume) { + int32_t currentIndex = player.getCurrentlyPlayingIndex(); + if (currentIndex >= 0) { + player.setGain(currentIndex, volume); + return; + } + + // Set gain for all tracks if nothing is playing. + for (int i = 0; i < 3; ++i) { + player.setGain(i, volume); + } +} + +/** + * Native (JNI) implementation of PowerPlayAudioPlayer.isOffloadedNative() + */ +JNIEXPORT jboolean JNICALL +Java_com_google_oboe_samples_powerplay_engine_PowerPlayAudioPlayer_isOffloadedNative( + JNIEnv *env, + jobject) { + return player.isOffloaded(); +} + +/** + * Native (JNI) implementation of PowerPlayAudioPlayer.getCurrentlyPlayingIndexNative() + */ +JNIEXPORT jint JNICALL +Java_com_google_oboe_samples_powerplay_engine_PowerPlayAudioPlayer_getCurrentlyPlayingIndexNative( + JNIEnv *env, + jobject) { + return player.getCurrentlyPlayingIndex(); +} + #ifdef __cplusplus } #endif + diff --git a/samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.cpp b/samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.cpp index 3cb50fa23..64f13643f 100644 --- a/samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.cpp +++ b/samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.cpp @@ -239,3 +239,11 @@ int32_t PowerPlayMultiPlayer::getBufferCapacityInFrames() { return mAudioStream->getBufferCapacityInFrames(); } + +bool PowerPlayMultiPlayer::isOffloaded() { + if (mAudioStream == nullptr) { + return false; + } + + return mAudioStream->getPerformanceMode() == PerformanceMode::PowerSavingOffloaded; +} diff --git a/samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.h b/samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.h index 319f71442..3b6cd3b46 100644 --- a/samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.h +++ b/samples/powerplay/src/main/cpp/PowerPlayMultiPlayer.h @@ -50,6 +50,8 @@ class PowerPlayMultiPlayer : public iolib::SimpleMultiPlayer { int32_t getBufferCapacityInFrames(); + bool isOffloaded(); + private: class MyPresentationCallback : public oboe::AudioStreamPresentationCallback { public: diff --git a/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/MainActivity.kt b/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/MainActivity.kt index ce42f9dc2..31f74b8b2 100644 --- a/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/MainActivity.kt +++ b/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/MainActivity.kt @@ -100,7 +100,12 @@ import com.google.oboe.samples.powerplay.engine.OboePerformanceMode import com.google.oboe.samples.powerplay.engine.PlayerState import com.google.oboe.samples.powerplay.engine.PowerPlayAudioPlayer import com.google.oboe.samples.powerplay.ui.theme.MusicPlayerTheme +import com.google.oboe.samples.powerplay.automation.IntentBasedTestSupport +import com.google.oboe.samples.powerplay.automation.IntentBasedTestSupport.LOG_TAG import kotlinx.coroutines.flow.distinctUntilChanged +import android.os.Handler +import android.os.Looper +import android.util.Log class MainActivity : ComponentActivity() { @@ -108,7 +113,23 @@ class MainActivity : ComponentActivity() { private lateinit var serviceIntent: Intent private var isMMapSupported: Boolean = false private var isOffloadSupported: Boolean = false - private var sampleRate: Int = 48000; + private var sampleRate: Int = 48000 + + // Automation state + private val handler = Handler(Looper.getMainLooper()) + private var autoStopRunnable: Runnable? = null + private var toggleOffloadRunnable: Runnable? = null + private var isAutomationMode = false + private var currentPerformanceMode: OboePerformanceMode = OboePerformanceMode.None + private var pendingAutomationIntent: Intent? = null + private var assetsLoaded = false + + // Shared UI state (updated by automation, observed by Compose) + private val isPlayingState = mutableStateOf(false) + private val songIndexState = mutableIntStateOf(0) + private val performanceModeState = mutableIntStateOf(0) + private val volumeState = mutableFloatStateOf(1.0f) + private val isMMapEnabledState = mutableStateOf(false) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -140,17 +161,220 @@ class MainActivity : ComponentActivity() { } } } + + // Process automation intent if provided + // Note: Intent processing is deferred until assets are loaded in SongScreen + pendingAutomationIntent = intent } override fun onDestroy() { super.onDestroy() - player.stopPlaying(0) + cancelScheduledTasks() + player.stopPlaying(player.getCurrentlyPlayingIndex().coerceAtLeast(0)) player.teardownAudioStream() } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + // If assets are loaded, process immediately; otherwise defer + if (assetsLoaded) { + processIntent(intent) + } else { + pendingAutomationIntent = intent + } + } + private fun setUpPowerPlayAudioPlayer() { player = PowerPlayAudioPlayer() player.setupAudioStream() + // Initialize shared UI state from player + isMMapEnabledState.value = player.isMMapEnabled() + } + + /** + * Process Intent extras for automation commands. + * Supports ADB commands like: + * adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + * --es command play --es perf_mode offload --ez background true + */ + private fun processIntent(intent: Intent?) { + val extras = intent?.extras ?: return + if (extras.isEmpty) return + + isAutomationMode = true + Log.i(LOG_TAG, "Processing automation intent") + + // Get playlist for song index validation + val playList = getPlayList() + + // Parse configuration from Intent + val songIndex = IntentBasedTestSupport.getSongIndex(extras, playList.size) + val perfMode = IntentBasedTestSupport.getPerformanceMode(extras) + val volume = IntentBasedTestSupport.getNormalizedVolume(extras) + val command = IntentBasedTestSupport.getCommand(extras) + val goBackground = IntentBasedTestSupport.isBackgroundRequested(extras) + val durationMs = IntentBasedTestSupport.getDurationMs(extras) + val useMMap = IntentBasedTestSupport.getMMapEnabled(extras, player.isMMapEnabled()) + val bufferFrames = IntentBasedTestSupport.getBufferFrames(extras) + val toggleOffload = IntentBasedTestSupport.isToggleOffloadRequested(extras) + val toggleIntervalMs = IntentBasedTestSupport.getToggleIntervalMs(extras) + + // Apply MMAP setting (must be done before playing) + if (useMMap != player.isMMapEnabled()) { + player.setMMapEnabled(useMMap) + isMMapEnabledState.value = useMMap + Log.i(LOG_TAG, "MMAP set to: $useMMap") + } + + // Store current performance mode + currentPerformanceMode = perfMode + + // Execute command + when (command?.lowercase()) { + IntentBasedTestSupport.COMMAND_PLAY -> { + // Stop any currently playing track first + val currentIndex = player.getCurrentlyPlayingIndex() + if (currentIndex >= 0) { + player.stopPlaying(currentIndex) + } + + player.startPlaying(songIndex, currentPerformanceMode) + + // Apply volume after starting (sample sources must exist) + player.setVolume(volume) + Log.i(LOG_TAG, "Volume set to: ${(volume * 100).toInt()}%") + + // Update shared UI state + isPlayingState.value = true + songIndexState.intValue = songIndex + performanceModeState.intValue = perfMode.value + volumeState.floatValue = volume + + logStatus( + IntentBasedTestSupport.STATUS_PLAYING, + "SONG" to songIndex, + "OFFLOAD" to player.isOffloaded(), + "MMAP" to player.isMMapEnabled() + ) + + // Apply buffer size (only effective in offload mode) + if (bufferFrames > 0 && currentPerformanceMode == OboePerformanceMode.PowerSavingOffloaded) { + val actualFrames = player.setBufferSizeInFrames(bufferFrames) + Log.i(LOG_TAG, "Buffer size set to: $actualFrames frames") + } + + // Schedule auto-stop if duration is set + if (durationMs > 0) { + scheduleAutoStop(songIndex, durationMs) + } + + // Schedule toggle offload stress test if requested + if (toggleOffload) { + scheduleToggleOffload(songIndex, toggleIntervalMs) + } + + // Move to background if requested + if (goBackground) { + moveTaskToBack(true) + Log.i(LOG_TAG, "Moved to background") + } + } + + IntentBasedTestSupport.COMMAND_PAUSE -> { + val currentIndex = player.getCurrentlyPlayingIndex() + if (currentIndex >= 0) { + player.stopPlaying(currentIndex) + isPlayingState.value = false + logStatus(IntentBasedTestSupport.STATUS_PAUSED) + } + cancelScheduledTasks() + } + + IntentBasedTestSupport.COMMAND_STOP -> { + val currentIndex = player.getCurrentlyPlayingIndex() + if (currentIndex >= 0) { + player.stopPlaying(currentIndex) + isPlayingState.value = false + logStatus(IntentBasedTestSupport.STATUS_STOPPED) + } + cancelScheduledTasks() + if (goBackground) { + finishAndRemoveTask() + } + } + + else -> { + // No command specified, just apply settings + Log.i(LOG_TAG, "No command specified, settings applied") + } + } + } + + private fun scheduleAutoStop(songIndex: Int, durationMs: Long) { + cancelAutoStop() + autoStopRunnable = Runnable { + player.stopPlaying(songIndex) + isPlayingState.value = false + logStatus(IntentBasedTestSupport.STATUS_STOPPED, "REASON" to "AUTO_STOP") + cancelScheduledTasks() + } + handler.postDelayed(autoStopRunnable!!, durationMs) + Log.i(LOG_TAG, "Auto-stop scheduled in ${durationMs}ms") + } + + private fun scheduleToggleOffload(songIndex: Int, intervalMs: Long) { + cancelToggleOffload() + var useOffload = currentPerformanceMode == OboePerformanceMode.PowerSavingOffloaded + + toggleOffloadRunnable = object : Runnable { + override fun run() { + useOffload = !useOffload + val newMode = if (useOffload) { + OboePerformanceMode.PowerSavingOffloaded + } else { + OboePerformanceMode.PowerSaving + } + + // Stop and restart with new mode + player.stopPlaying(songIndex) + player.startPlaying(songIndex, newMode) + + // Update UI state + performanceModeState.intValue = newMode.value + + logStatus( + IntentBasedTestSupport.STATUS_PLAYING, + "TOGGLE" to "OFFLOAD", + "OFFLOAD" to player.isOffloaded() + ) + + // Schedule next toggle + handler.postDelayed(this, intervalMs) + } + } + handler.postDelayed(toggleOffloadRunnable!!, intervalMs) + Log.i(LOG_TAG, "Toggle offload stress test started (interval: ${intervalMs}ms)") + } + + private fun cancelAutoStop() { + autoStopRunnable?.let { handler.removeCallbacks(it) } + autoStopRunnable = null + } + + private fun cancelToggleOffload() { + toggleOffloadRunnable?.let { handler.removeCallbacks(it) } + toggleOffloadRunnable = null + } + + private fun cancelScheduledTasks() { + cancelAutoStop() + cancelToggleOffload() + } + + private fun logStatus(status: String, vararg extras: Pair) { + val message = IntentBasedTestSupport.formatStatusLog(status, *extras) + Log.i(LOG_TAG, message) } /*** @@ -162,19 +386,21 @@ class MainActivity : ComponentActivity() { fun SongScreen() { val playList = getPlayList() val pagerState = rememberPagerState(pageCount = { playList.count() }) - val playingSongIndex = remember { - mutableIntStateOf(0) - } - val offload = remember { - mutableIntStateOf(0) // 0: None, 1: Low Latency, 2: Power Saving, 3: PCM Offload - } - - val isMMapEnabled = remember { mutableStateOf(player.isMMapEnabled()) } - val isPlaying = remember { - mutableStateOf(false) + + // Use shared state from class level (automation can update these) + val playingSongIndex = songIndexState + val offload = performanceModeState + val isPlaying = isPlayingState + val isMMapEnabled = isMMapEnabledState + + var sliderPosition by remember { mutableFloatStateOf(volumeState.floatValue) } + + // Sync pager with song index when automation changes it + LaunchedEffect(playingSongIndex.intValue) { + if (pagerState.currentPage != playingSongIndex.intValue) { + pagerState.animateScrollToPage(playingSongIndex.intValue) + } } - var sliderPosition by remember { mutableFloatStateOf(0f) } - LaunchedEffect(pagerState) { snapshotFlow { pagerState.currentPage } @@ -195,6 +421,12 @@ class MainActivity : ComponentActivity() { player.loadFile(assets, it.fileName, index) player.setLooping(index, true) } + // Assets are now loaded, process any pending automation intent + assetsLoaded = true + pendingAutomationIntent?.let { + processIntent(it) + pendingAutomationIntent = null + } } @@ -253,8 +485,10 @@ class MainActivity : ComponentActivity() { val radioOptions = mutableListOf("None", "Low Latency", "Power Saving") if (isOffloadSupported) radioOptions.add("PCM Offload") - val (selectedOption, onOptionSelected) = remember { - mutableStateOf(radioOptions[0]) + // Derive selectedOption from shared offload state + val selectedOption = radioOptions.getOrElse(offload.intValue) { radioOptions[0] } + val onOptionSelected: (String) -> Unit = { selected -> + offload.intValue = radioOptions.indexOf(selected).coerceAtLeast(0) } val enabled = !isPlaying.value radioOptions.forEachIndexed { index, text -> @@ -267,7 +501,6 @@ class MainActivity : ComponentActivity() { onClick = { if (enabled) { onOptionSelected(text) - offload.intValue = index } }, role = Role.RadioButton diff --git a/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/automation/IntentBasedTestSupport.kt b/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/automation/IntentBasedTestSupport.kt new file mode 100644 index 000000000..a484c4de7 --- /dev/null +++ b/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/automation/IntentBasedTestSupport.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2026 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 com.google.oboe.samples.powerplay.automation + +import android.os.Bundle +import com.google.oboe.samples.powerplay.engine.OboePerformanceMode + +/** + * Constants and utilities for Intent-based automation of the PowerPlay sample app. + * + * This enables ADB command-line control for testing power-efficient audio features. + * + * Example usage: + * ``` + * adb shell am start -n com.google.oboe.samples.powerplay/.MainActivity \ + * --es command play \ + * --es perf_mode offload \ + * --ez background true + * ``` + */ +object IntentBasedTestSupport { + + // ============================================================================ + // Command Keys - Intent extra names for ADB commands + // ============================================================================ + + /** Command to execute: "play", "pause", "stop" */ + const val KEY_COMMAND = "command" + + /** Song index to play (0-2 for built-in playlist) */ + const val KEY_SONG_INDEX = "song_index" + + /** Performance mode: "none", "lowlat", "powersave", "offload" */ + const val KEY_PERF_MODE = "perf_mode" + + /** Volume percentage (0-100) */ + const val KEY_VOLUME = "volume" + + /** Move activity to background after starting playback */ + const val KEY_BACKGROUND = "background" + + /** Duration in milliseconds - auto-stop after this time */ + const val KEY_DURATION_MS = "duration_ms" + + /** Enable/disable MMAP audio path */ + const val KEY_USE_MMAP = "use_mmap" + + /** Buffer size in frames (only applicable in PCM Offload mode) */ + const val KEY_BUFFER_FRAMES = "buffer_frames" + + /** Enable toggle offload stress test mode */ + const val KEY_TOGGLE_OFFLOAD = "toggle_offload" + + /** Toggle interval in milliseconds for stress test */ + const val KEY_TOGGLE_INTERVAL_MS = "toggle_interval_ms" + + // ============================================================================ + // Command Values + // ============================================================================ + + const val COMMAND_PLAY = "play" + const val COMMAND_PAUSE = "pause" + const val COMMAND_STOP = "stop" + + // ============================================================================ + // Performance Mode Values + // ============================================================================ + + const val PERF_MODE_NONE = "none" + const val PERF_MODE_LOW_LATENCY = "lowlat" + const val PERF_MODE_POWER_SAVING = "powersave" + const val PERF_MODE_OFFLOAD = "offload" + + // ============================================================================ + // Default Values + // ============================================================================ + + const val DEFAULT_SONG_INDEX = 0 + const val DEFAULT_VOLUME = 100 + const val DEFAULT_DURATION_MS = -1 // No auto-stop + const val DEFAULT_TOGGLE_INTERVAL_MS = 5000L + + // ============================================================================ + // Status Log Tags for Machine Parsing + // ============================================================================ + + /** Log tag for PowerPlay automation */ + const val LOG_TAG = "PowerPlay" + + /** Prefix for machine-readable status logs */ + const val STATUS_PREFIX = "POWERPLAY_STATUS:" + + // Status values + const val STATUS_PLAYING = "PLAYING" + const val STATUS_PAUSED = "PAUSED" + const val STATUS_STOPPED = "STOPPED" + const val STATUS_ERROR = "ERROR" + const val STATUS_OFFLOAD_REVOKED = "OFFLOAD_REVOKED" + + // ============================================================================ + // Utility Functions + // ============================================================================ + + /** + * Parse performance mode string from Intent extra to OboePerformanceMode enum. + * + * @param text Performance mode string ("none", "lowlat", "powersave", "offload") + * @return Corresponding OboePerformanceMode, defaults to None if invalid + */ + fun getPerformanceModeFromText(text: String?): OboePerformanceMode { + return when (text?.lowercase()) { + PERF_MODE_NONE -> OboePerformanceMode.None + PERF_MODE_LOW_LATENCY -> OboePerformanceMode.LowLatency + PERF_MODE_POWER_SAVING -> OboePerformanceMode.PowerSaving + PERF_MODE_OFFLOAD -> OboePerformanceMode.PowerSavingOffloaded + else -> OboePerformanceMode.None + } + } + + /** + * Get song index from bundle with bounds checking. + * + * @param bundle Intent extras bundle + * @param maxIndex Maximum valid index (exclusive) + * @return Valid song index, clamped to valid range + */ + fun getSongIndex(bundle: Bundle, maxIndex: Int): Int { + val index = bundle.getInt(KEY_SONG_INDEX, DEFAULT_SONG_INDEX) + return index.coerceIn(0, maxIndex - 1) + } + + /** + * Get volume from bundle, normalized to 0.0-1.0 range. + * + * @param bundle Intent extras bundle + * @return Volume as float (0.0 to 1.0) + */ + fun getNormalizedVolume(bundle: Bundle): Float { + val volume = bundle.getInt(KEY_VOLUME, DEFAULT_VOLUME) + return (volume.coerceIn(0, 100) / 100.0f) + } + + /** + * Get command from bundle. + * + * @param bundle Intent extras bundle + * @return Command string or null if not present + */ + fun getCommand(bundle: Bundle): String? { + return bundle.getString(KEY_COMMAND) + } + + /** + * Get performance mode from bundle. + * + * @param bundle Intent extras bundle + * @return OboePerformanceMode from bundle, defaults to None + */ + fun getPerformanceMode(bundle: Bundle): OboePerformanceMode { + return getPerformanceModeFromText(bundle.getString(KEY_PERF_MODE)) + } + + /** + * Check if background mode is requested. + * + * @param bundle Intent extras bundle + * @return true if app should move to background after starting + */ + fun isBackgroundRequested(bundle: Bundle): Boolean { + return bundle.getBoolean(KEY_BACKGROUND, false) + } + + /** + * Get duration in milliseconds for auto-stop. + * + * @param bundle Intent extras bundle + * @return Duration in ms, or DEFAULT_DURATION_MS (-1) if not set + */ + fun getDurationMs(bundle: Bundle): Long { + return bundle.getInt(KEY_DURATION_MS, DEFAULT_DURATION_MS).toLong() + } + + /** + * Get MMAP preference from bundle. + * + * @param bundle Intent extras bundle + * @param currentValue Current MMAP enabled state + * @return MMAP preference, or current value if not specified + */ + fun getMMapEnabled(bundle: Bundle, currentValue: Boolean): Boolean { + return if (bundle.containsKey(KEY_USE_MMAP)) { + bundle.getBoolean(KEY_USE_MMAP) + } else { + currentValue + } + } + + /** + * Get buffer size in frames from bundle. + * + * @param bundle Intent extras bundle + * @return Buffer size in frames, or 0 if not specified + */ + fun getBufferFrames(bundle: Bundle): Int { + return bundle.getInt(KEY_BUFFER_FRAMES, 0) + } + + /** + * Check if toggle offload stress test is requested. + * + * @param bundle Intent extras bundle + * @return true if toggle offload mode should be enabled + */ + fun isToggleOffloadRequested(bundle: Bundle): Boolean { + return bundle.getBoolean(KEY_TOGGLE_OFFLOAD, false) + } + + /** + * Get toggle interval for stress test. + * + * @param bundle Intent extras bundle + * @return Toggle interval in milliseconds + */ + fun getToggleIntervalMs(bundle: Bundle): Long { + return bundle.getInt(KEY_TOGGLE_INTERVAL_MS, DEFAULT_TOGGLE_INTERVAL_MS.toInt()).toLong() + } + + /** + * Format a machine-readable status log message. + * + * @param status Status value (e.g., STATUS_PLAYING) + * @param extras Additional key=value pairs to include + * @return Formatted log message + */ + fun formatStatusLog(status: String, vararg extras: Pair): String { + val extraStr = if (extras.isNotEmpty()) { + " | " + extras.joinToString(" | ") { "${it.first}=${it.second}" } + } else { + "" + } + return "$STATUS_PREFIX $status$extraStr" + } +} diff --git a/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/engine/PowerPlayAudioPlayer.kt b/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/engine/PowerPlayAudioPlayer.kt index 329e6cdbf..bd9398e66 100644 --- a/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/engine/PowerPlayAudioPlayer.kt +++ b/samples/powerplay/src/main/kotlin/com/google/oboe/samples/powerplay/engine/PowerPlayAudioPlayer.kt @@ -93,6 +93,27 @@ class PowerPlayAudioPlayer() : DefaultLifecycleObserver { fun getBufferCapacityInFrames(): Int = getBufferCapacityInFramesNative() + /** + * Sets the playback volume (gain) for the audio stream. + * + * @param volume Volume level from 0.0 (mute) to 1.0 (full volume) + */ + fun setVolume(volume: Float) = setVolumeNative(volume) + + /** + * Checks if the current audio stream is using PCM Offload. + * + * @return true if offload is active, false otherwise + */ + fun isOffloaded(): Boolean = isOffloadedNative() + + /** + * Gets the index of the currently playing track. + * + * @return Track index (0-based) or -1 if nothing is playing + */ + fun getCurrentlyPlayingIndex(): Int = getCurrentlyPlayingIndexNative() + /** * Native functions. * Load the library containing the native code including the JNI functions. @@ -116,6 +137,9 @@ class PowerPlayAudioPlayer() : DefaultLifecycleObserver { private external fun isMMapSupportedNative(): Boolean private external fun setBufferSizeInFramesNative(bufferSizeInFrames: Int): Int private external fun getBufferCapacityInFramesNative(): Int + private external fun setVolumeNative(volume: Float) + private external fun isOffloadedNative(): Boolean + private external fun getCurrentlyPlayingIndexNative(): Int /** * Companion