blob: d6fa70cdd278b855d722a9b58806c52afcb50684 [file] [log] [blame]
/*
* Copyright (C) 2020 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.musicrecognition.cts;
import static androidx.test.InstrumentationRegistry.getContext;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import android.app.AppOpsManager;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaMetadata;
import android.media.MediaRecorder;
import android.media.musicrecognition.MusicRecognitionManager;
import android.media.musicrecognition.RecognitionRequest;
import android.os.Binder;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.RequiredServiceRule;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Tests for {@link MusicRecognitionManager}.
*/
@RunWith(AndroidJUnit4.class)
public class MusicRecognitionManagerTest {
private static final String TAG = MusicRecognitionManagerTest.class.getSimpleName();
private static final long VERIFY_TIMEOUT_MS = 40_000;
private static final long VERIFY_APPOP_CHANGE_TIMEOUT_MS = 10000;
@Rule public TestName mTestName = new TestName();
@Rule
public final RequiredServiceRule mRequiredServiceRule =
new RequiredServiceRule(Context.MUSIC_RECOGNITION_SERVICE);
private MusicRecognitionManager mManager;
private CtsMusicRecognitionService.Watcher mWatcher;
@Mock MusicRecognitionManager.RecognitionCallback mCallback;
@Captor ArgumentCaptor<Bundle> mBundleCaptor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
// Grant permission to call the api.
escalateTestPermissions();
mManager = getContext().getSystemService(MusicRecognitionManager.class);
mWatcher = CtsMusicRecognitionService.setWatcher();
// Tell MusicRecognitionManagerService to use our no-op service instead.
setService(CtsMusicRecognitionService.SERVICE_COMPONENT.flattenToString());
}
@After
public void tearDown() {
resetService();
mWatcher = null;
CtsMusicRecognitionService.clearWatcher();
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
@Test
public void testRecognitionRequest() {
AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.MIC, 16_000,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, 256_000);
RecognitionRequest request = new RecognitionRequest.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setInternalCapturePreset(MediaRecorder.AudioSource.MIC)
.build())
.setAudioFormat(record.getFormat())
.setCaptureSession(record.getAudioSessionId())
.setMaxAudioLengthSeconds(8)
// Drop the first second of audio.
.setIgnoreBeginningFrames(16_000)
.build();
assertThat(request.getAudioFormat()).isEqualTo(record.getFormat());
assertThat(request.getMaxAudioLengthSeconds()).isEqualTo(8);
assertThat(request.getCaptureSession()).isEqualTo(record.getAudioSessionId());
assertThat(request.getIgnoreBeginningFrames()).isEqualTo(16_000);
assertThat(request.getAudioAttributes()).isEqualTo(record.getAudioAttributes());
}
@Test
public void testOnRecognitionFailed() throws Exception {
mWatcher.failureCode = MusicRecognitionManager.RECOGNITION_FAILED_NO_CONNECTIVITY;
invokeMusicRecognitionApi();
verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onAudioStreamClosed();
verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onRecognitionFailed(any(),
eq(MusicRecognitionManager.RECOGNITION_FAILED_NO_CONNECTIVITY));
verify(mCallback, never()).onRecognitionSucceeded(any(), any(), any());
}
@Test
public void testOnRecognitionSucceeded() throws Exception {
mWatcher.result = new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_ARTIST, "artist")
.putString(MediaMetadata.METADATA_KEY_TITLE, "title")
.build();
RecognitionRequest request = invokeMusicRecognitionApi();
verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onAudioStreamClosed();
verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onRecognitionSucceeded(eq(request),
eq(mWatcher.result), eq(null));
verify(mCallback, never()).onRecognitionFailed(any(), anyInt());
// 8 seconds minus 16k frames dropped from the beginning.
assertThat(mWatcher.stream).hasLength(256_000 - 32_000);
}
@Test
public void testRemovesBindersFromBundle() throws Exception {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
mWatcher.result = new MediaMetadata.Builder().build();
mWatcher.resultExtras = new Bundle();
mWatcher.resultExtras.putString("stringKey", "stringValue");
mWatcher.resultExtras.putBinder("binderKey", new Binder());
mWatcher.resultExtras.putParcelable("fdKey", pipe[0]);
RecognitionRequest request = invokeMusicRecognitionApi();
verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onAudioStreamClosed();
verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onRecognitionSucceeded(eq(request),
eq(mWatcher.result), mBundleCaptor.capture());
assertThat(mBundleCaptor.getValue().getString("stringKey")).isEqualTo("stringValue");
// Binder and file descriptor removed.
assertThat(mBundleCaptor.getValue().size()).isEqualTo(1);
pipe[0].close();
pipe[1].close();
}
/**
* Verifies the shell override is only allowed when the caller of the api is also the owner of
* the override service.
*/
@Test
public void testDoesntBindToForeignService() {
setService(
"android.musicrecognition.cts2/android.musicrecognition.cts2"
+ ".OutsideOfPackageService");
invokeMusicRecognitionApi();
verify(mCallback, timeout(VERIFY_TIMEOUT_MS)).onRecognitionFailed(any(),
eq(MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_UNAVAILABLE));
verify(mCallback, never()).onRecognitionSucceeded(any(), any(), any());
}
@Test
public void testRecordAudioOpsAreTracked() {
mWatcher.result = new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_ARTIST, "artist")
.putString(MediaMetadata.METADATA_KEY_TITLE, "title")
.build();
final String packageName = CtsMusicRecognitionService.SERVICE_PACKAGE;
final int uid = Process.myUid();
final Context context = getInstrumentation().getContext();
final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
final AppOpsManager.OnOpActiveChangedListener listener = mock(
AppOpsManager.OnOpActiveChangedListener.class);
// Assert the app op is not started
assertFalse(appOpsManager.isOpActive(AppOpsManager.OPSTR_RECORD_AUDIO, uid, packageName));
// Start watching for record audio op
appOpsManager.startWatchingActive(new String[] { AppOpsManager.OPSTR_RECORD_AUDIO },
context.getMainExecutor(), listener);
// Invoke API
RecognitionRequest request = invokeMusicRecognitionApi();
// The app op should start
String expectedAttributionTag = "CtsMusicRecognitionAttributionTag";
verify(listener, timeout(VERIFY_APPOP_CHANGE_TIMEOUT_MS))
.onOpActiveChanged(eq(AppOpsManager.OPSTR_RECORD_AUDIO),
eq(uid), eq(packageName), eq(expectedAttributionTag), eq(true),
anyInt(), anyInt());
// Wait for streaming to finish.
reset(listener);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// The app op should finish
verify(listener, timeout(VERIFY_APPOP_CHANGE_TIMEOUT_MS))
.onOpActiveChanged(eq(AppOpsManager.OPSTR_RECORD_AUDIO),
eq(uid), eq(packageName), eq(expectedAttributionTag), eq(false),
anyInt(), anyInt());
// Start with a clean slate
reset(listener);
// Stop watching for app op
appOpsManager.stopWatchingActive(listener);
// No other callbacks expected
verify(listener, timeout(VERIFY_APPOP_CHANGE_TIMEOUT_MS).times(0))
.onOpActiveChanged(eq(AppOpsManager.OPSTR_RECORD_AUDIO),
anyInt(), anyString(), anyBoolean());
}
private RecognitionRequest invokeMusicRecognitionApi() {
Log.d(TAG, "Invoking service.");
AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.MIC, 16_000,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, 256_000);
RecognitionRequest request = new RecognitionRequest.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setInternalCapturePreset(MediaRecorder.AudioSource.MIC)
.build())
.setAudioFormat(record.getFormat())
.setCaptureSession(record.getAudioSessionId())
.setMaxAudioLengthSeconds(8)
// Drop the first second of audio.
.setIgnoreBeginningFrames(16_000)
.build();
mManager.beginStreamingSearch(
request,
MoreExecutors.directExecutor(),
mCallback);
Log.d(TAG, "Invoking service done.");
return request;
}
/**
* Sets the music recognition service.
*/
private static void setService(@NonNull String service) {
Log.d(TAG, "Setting music recognition service to " + service);
int userId = android.os.Process.myUserHandle().getIdentifier();
runShellCommand(
"cmd music_recognition set temporary-service %d %s 60000", userId, service);
}
private static void resetService() {
Log.d(TAG, "Resetting music recognition service");
int userId = android.os.Process.myUserHandle().getIdentifier();
runShellCommand("cmd music_recognition set temporary-service %d", userId);
}
private static void escalateTestPermissions() {
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
"android.permission.MANAGE_MUSIC_RECOGNITION");
}
}