diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/.gitignore b/libwvdrmengine/test/java/FullDecryptPathTesting/.gitignore
new file mode 100644
index 00000000..db32c745
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/.gitignore
@@ -0,0 +1,95 @@
+# Android studio files:
+local.properties
+.gradle/
+build/
+captures/
+gradle-app.setting
+.externalNativeBuild
+*.iml
+vcs.xml
+
+# Created by https://www.gitignore.io/api/intellij
+# Edit at https://www.gitignore.io/?templates=intellij
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+# End of https://www.gitignore.io/api/intellij
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/.idea/modules.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/.idea/modules.xml
new file mode 100644
index 00000000..b6f7f652
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/.idea/runConfigurations.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/.idea/runConfigurations.xml
new file mode 100644
index 00000000..7f68460d
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/FullDecryptPathTesting.apk b/libwvdrmengine/test/java/FullDecryptPathTesting/FullDecryptPathTesting.apk
new file mode 100644
index 00000000..c429c100
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/FullDecryptPathTesting.apk differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/README.md b/libwvdrmengine/test/java/FullDecryptPathTesting/README.md
new file mode 100644
index 00000000..659259dc
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/README.md
@@ -0,0 +1,90 @@
+# Full Decrypt Path Testing Application
+
+Released August, 2019.
+
+This is the Full Decrypt Path Testing application. The application is used
+to test the full decrypt path of OEMCrypto.
+
+The app computes a hash of a clear frame, encrypts the frame, and sends both to
+OEMCrypto. OEMCrypto should decrypt, compute a hash, and then verify the
+hash. The app will display any errors in the hash.
+
+## Getting Started
+
+If OEMCrypto on the device supports CRC32 hash, then the application should work
+"out of the box". Install it using
+ adb install FullDecryptPathTesting.apk
+
+To start a batch of tests, click on the "Start" button. The device needs to be
+connected to WiFi to fetch a license. Once the license is installed, it should
+cycle through a set of predefined tests and a bunch of random tests. The video
+surface should display a single frame, which is the FDPT logo.
+
+Press the "Clear" button to clear the logs if you want to run the tests again.
+
+## Settings
+
+To change which test to run, click on the "Setup" button. The default settings
+run two of the four standard modes, "cenc" and "cbcs" with 5000 randomly
+generated tests for each mode.
+
+## Running on a production device
+
+This application is intended to test decryption on a device running the full
+Android stack. However, most Level 1 OEMCrypto implementations will not support
+the decrypt hash feature on a production device. The feature will be optimized
+out on production devices. For this reason, it should not be suprising if no
+tests run when the settings for "Use Level 1" and "Use Secure Buffer" are set to
+true.
+
+## Running the application from adb
+
+The settings filed are stored on the device in the shared preferences
+directory. You can pull the XML file from the device, edit it, and push it back
+to the device:
+```shell
+DIR=/data/data/com.google.widevine.fulldecryptpathtesting/shared_prefs
+FILE=com.google.widevine.fulldecryptpathtesting_preferences.xml
+adb pull $DIR/$FILE
+```
+Now edit the xml file on your host computer. You can then push the file back
+to the directory. You might have to force-stop the application first, to force
+the preferences to be read again.
+```shell
+adb shell am force-stop com.google.widevine.fulldecryptpathtesting
+adb push $FILE $DIR/$FILE
+```
+Now that the settings have been updated, you can start the application from the
+command line:
+```shell
+adb shell am start -n "com.google.widevine.fulldecryptpathtesting/.MainActivity" \
+ -d "start"
+```
+
+## Repeatable Tests
+
+If you want to re-run a test with the exact same test cases, you may change the random seed
+in the settings. Each run prints the random seed that it uses in the logcat.
+If you don't set the seed, the application picks a random seed using a secure
+random number generator.
+
+## Source Code
+
+The app builds with Android Studio.
+
+Vendors who wish to supply their own hashing function instead of using CRC32
+should edit the file HashGenerator.java or the C++ file native-lib.cpp.
+
+BUILD ERRORS:
+If you see the error below you might not have CMake installed.
+
+```shell
+>> * What went wrong:
+>> A problem occurred configuring project ':mobile'.
+>> > java.lang.NullPointerException (no error message)
+```
+
+Because the app includes native code written in C++, it requires CMake. From
+the tools menu, select Android and Android SDK Manager. On the tab "SDK Tools",
+make sure that Cmake is selected.
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/build.gradle b/libwvdrmengine/test/java/FullDecryptPathTesting/build.gradle
new file mode 100644
index 00000000..8d3ef8e5
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.2.1'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/com.google.widevine.fulldecryptpathtesting_preferences.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/com.google.widevine.fulldecryptpathtesting_preferences.xml
new file mode 100644
index 00000000..a4c366e4
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/com.google.widevine.fulldecryptpathtesting_preferences.xml
@@ -0,0 +1,16 @@
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/gradle.properties b/libwvdrmengine/test/java/FullDecryptPathTesting/gradle.properties
new file mode 100644
index 00000000..aac7c9b4
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/gradle/wrapper/gradle-wrapper.jar b/libwvdrmengine/test/java/FullDecryptPathTesting/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..13372aef
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/gradle/wrapper/gradle-wrapper.properties b/libwvdrmengine/test/java/FullDecryptPathTesting/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..29852267
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jan 29 15:40:42 PST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/gradlew b/libwvdrmengine/test/java/FullDecryptPathTesting/gradlew
new file mode 100755
index 00000000..9d82f789
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/gradlew.bat b/libwvdrmengine/test/java/FullDecryptPathTesting/gradlew.bat
new file mode 100644
index 00000000..aec99730
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/.gitignore b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/CMakeLists.txt b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/CMakeLists.txt
new file mode 100644
index 00000000..43f48a21
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/CMakeLists.txt
@@ -0,0 +1,46 @@
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html
+
+# Sets the minimum version of CMake required to build the native library.
+
+cmake_minimum_required(VERSION 3.4.1)
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+
+add_library( # Sets the name of the library.
+ native-lib
+
+ # Sets the library as a shared library.
+ SHARED
+
+ # Provides a relative path to your source file(s).
+ src/main/cpp/native-lib.cpp
+ src/main/cpp/wvcrc.cpp
+ )
+
+# Searches for a specified prebuilt library and stores the path as a
+# variable. Because CMake includes system libraries in the search path by
+# default, you only need to specify the name of the public NDK library
+# you want to add. CMake verifies that the library exists before
+# completing its build.
+
+find_library( # Sets the name of the path variable.
+ log-lib
+
+ # Specifies the name of the NDK library that
+ # you want CMake to locate.
+ log )
+
+# Specifies libraries CMake should link to your target library. You
+# can link multiple libraries, such as libraries you define in this
+# build script, prebuilt third-party libraries, or system libraries.
+
+target_link_libraries( # Specifies the target library.
+ native-lib
+
+ # Links the target library to the log library
+ # included in the NDK.
+ ${log-lib} )
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/build.gradle b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/build.gradle
new file mode 100644
index 00000000..b69a8cf7
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/build.gradle
@@ -0,0 +1,40 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ applicationId "com.google.widevine.fulldecryptpathtesting"
+ minSdkVersion 24
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ externalNativeBuild {
+ cmake {
+ cppFlags "-std=c++14"
+ }
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path "CMakeLists.txt"
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'com.android.support:appcompat-v7:27.1.1'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+ implementation 'com.android.support:support-v4:26.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test:rules:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/proguard-rules.pro b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/androidTest/java/com/google/widevine/fulldecryptpathtesting/FullDecryptPathTest.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/androidTest/java/com/google/widevine/fulldecryptpathtesting/FullDecryptPathTest.java
new file mode 100644
index 00000000..b04ff590
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/androidTest/java/com/google/widevine/fulldecryptpathtesting/FullDecryptPathTest.java
@@ -0,0 +1,96 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import static org.junit.Assert.*;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceView;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for the FrameGenerator.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class FullDecryptPathTest {
+
+ @Rule
+ public final ActivityTestRule mActivityRule =
+ new ActivityTestRule<>(MainActivity.class);
+
+ @Test
+ public void initTest() throws Exception {
+ // Context of the app under test.
+ Context context = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.google.widevine.fulldecryptpathtesting", context.getPackageName());
+
+ Logger logger = new Logger();
+ FrameGenerator frameGenerator = new FrameGenerator(context, logger);
+ frameGenerator.prepareKeyFrame();
+ assertTrue(0 < frameGenerator.getMinimumFrameSize());
+ }
+
+ @Test
+ public void sizeTest() throws Exception {
+ // Context of the app under test.
+ Context context = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.google.widevine.fulldecryptpathtesting", context.getPackageName());
+
+ Logger logger = new Logger();
+ FrameGenerator frameGenerator = new FrameGenerator(context, logger);
+ frameGenerator.prepareKeyFrame();
+ assertTrue(0 < frameGenerator.getMinimumFrameSize());
+ int minSize = frameGenerator.getMinimumFrameSize();
+ for (int i = 0; i < 1000; i++) {
+ int size = minSize + i;
+ ITestFrameBuilder fb = new ByteArrayFrameBuilder(size);
+ byte[] frame = frameGenerator.buildtestFrame(fb);
+ assertEquals(size, frame.length);
+ }
+ // And one big one:
+ int size = 4 * 1024 * 1024;
+ ITestFrameBuilder fb = new ByteArrayFrameBuilder(size);
+ byte[] frame = frameGenerator.buildtestFrame(fb);
+ assertEquals(size, frame.length);
+ }
+
+ @Test
+ public void testAvcLevel1() throws InterruptedException {
+ testFullDecryptPath(CodecHandler.MIME_TYPE, true);
+ }
+
+ @Test
+ public void testAvcLevel3() throws InterruptedException {
+ testFullDecryptPath(CodecHandler.MIME_TYPE, false);
+ }
+
+ private void testFullDecryptPath(String mimeType, boolean useLevel1)
+ throws InterruptedException {
+ Logger logger = new Logger();
+ Activity activity = mActivityRule.getActivity();
+ SurfaceView surfaceView = (SurfaceView) activity.findViewById(R.id.playback_view);
+ TestParameters parameters = new TestParameters();
+ parameters.setUseLevel1(useLevel1);
+ parameters.setCodecName(MediaUtil.getCodecNameForMime(mimeType, useLevel1));
+ TestRunner runner = new TestRunner(logger, activity, surfaceView, parameters);
+ logger.setActivity(activity);
+ try {
+ IWorker worker = new InstrumentationTestWorker();
+ runner.doTest(worker);
+ } finally {
+ runner.cleanUp();
+ }
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/androidTest/java/com/google/widevine/fulldecryptpathtesting/InstrumentationTestWorker.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/androidTest/java/com/google/widevine/fulldecryptpathtesting/InstrumentationTestWorker.java
new file mode 100644
index 00000000..fb8bc05d
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/androidTest/java/com/google/widevine/fulldecryptpathtesting/InstrumentationTestWorker.java
@@ -0,0 +1,25 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+class InstrumentationTestWorker implements IWorker {
+ @Override
+ public void maybePause() {}
+
+ @Override
+ public boolean finishEarly() {
+ return false;
+ }
+
+ @Override
+ public void handleException(String message, Exception ex) {
+ throw new RuntimeException(message, ex);
+ }
+
+ @Override
+ public void handleError(String message) {
+ throw new RuntimeException(message);
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/AndroidManifest.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..9e719a41
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+ android:supportsRtl="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/native-lib.cpp b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/native-lib.cpp
new file mode 100644
index 00000000..1726fc00
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/native-lib.cpp
@@ -0,0 +1,34 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+#include
+#include
+#include
+#include
+
+#include "wvcrc32.h"
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_google_widevine_fulldecryptpathtesting_HashGenerator_computeCRC(
+ JNIEnv *env, jobject /* this */, jbyteArray jframe, jbyteArray jhash) {
+ jbyte *frame = env->GetByteArrayElements(jframe, NULL);
+ jsize frame_size = env->GetArrayLength(jframe);
+
+ jbyte *hash = env->GetByteArrayElements(jhash, NULL);
+ jsize hash_size = env->GetArrayLength(jhash);
+
+ if (!frame || !hash) {
+ __android_log_write(ANDROID_LOG_ERROR, "FDPT_CRC",
+ "Null pointer passed to computeCRC");
+ } else if (hash_size != sizeof(uint32_t)) {
+ __android_log_write(ANDROID_LOG_ERROR, "FDPT_CRC",
+ "Hash size was not 4 bytes.");
+ } else {
+ uint32_t *hash_value = reinterpret_cast(hash);
+ uint32_t computed_hash = wvoec::wvcrc32n(reinterpret_cast(frame), frame_size);
+ *hash_value = htonl(computed_hash);
+ }
+ if (hash) env->ReleaseByteArrayElements(jhash, hash, 0);
+ if (frame) env->ReleaseByteArrayElements(jframe, frame, 0);
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/wvcrc.cpp b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/wvcrc.cpp
new file mode 100644
index 00000000..d7cb8cd5
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/wvcrc.cpp
@@ -0,0 +1,108 @@
+// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+//
+// Compute CRC32 Checksum. Needed for verification of WV Keybox.
+//
+#include "wvcrc32.h"
+#include
+
+namespace wvoec {
+
+#define INIT_CRC32 0xffffffff
+
+uint32_t wvrunningcrc32(const uint8_t* p_begin, int i_count, uint32_t i_crc) {
+ static uint32_t CRC32[256] = {
+ 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9,
+ 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
+ 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
+ 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
+ 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9,
+ 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
+ 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011,
+ 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
+ 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
+ 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
+ 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81,
+ 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
+ 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49,
+ 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
+ 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
+ 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
+ 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae,
+ 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
+ 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16,
+ 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
+ 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
+ 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
+ 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066,
+ 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
+ 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e,
+ 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
+ 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
+ 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
+ 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e,
+ 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
+ 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686,
+ 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
+ 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
+ 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
+ 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f,
+ 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
+ 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47,
+ 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
+ 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
+ 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
+ 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7,
+ 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
+ 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f,
+ 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
+ 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
+ 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
+ 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f,
+ 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
+ 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
+ 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
+ 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
+ 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
+ 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30,
+ 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
+ 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088,
+ 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
+ 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
+ 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
+ 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
+ 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
+ 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0,
+ 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
+ 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
+ 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
+ };
+
+ /* Calculate the CRC */
+ while (i_count > 0) {
+ i_crc = (i_crc << 8) ^ CRC32[(i_crc >> 24) ^ ((uint32_t) * p_begin)];
+ p_begin++;
+ i_count--;
+ }
+
+ return(i_crc);
+}
+
+uint32_t wvcrc32(const uint8_t* p_begin, int i_count) {
+ return(wvrunningcrc32(p_begin, i_count, INIT_CRC32));
+}
+
+uint32_t wvcrc32Init() {
+ return INIT_CRC32;
+}
+
+uint32_t wvcrc32Cont(const uint8_t* p_begin, int i_count, uint32_t prev_crc) {
+ return(wvrunningcrc32(p_begin, i_count, prev_crc));
+}
+
+uint32_t wvcrc32n(const uint8_t* p_begin, int i_count) {
+ return htonl(wvrunningcrc32(p_begin, i_count, INIT_CRC32));
+}
+
+} // namespace wvoec
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/wvcrc32.h b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/wvcrc32.h
new file mode 100644
index 00000000..24362119
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/cpp/wvcrc32.h
@@ -0,0 +1,23 @@
+// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+//
+// Compute CRC32 Checksum. Needed for verification of WV Keybox.
+//
+#ifndef CDM_WVCRC32_H_
+#define CDM_WVCRC32_H_
+
+#include
+
+namespace wvoec {
+
+uint32_t wvcrc32(const uint8_t* p_begin, int i_count);
+uint32_t wvcrc32Init();
+uint32_t wvcrc32Cont(const uint8_t* p_begin, int i_count, uint32_t prev_crc);
+
+// Convert to network byte order
+uint32_t wvcrc32n(const uint8_t* p_begin, int i_count);
+
+} // namespace wvoec
+
+#endif // CDM_WVCRC32_H_
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/ic_launcher-web.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/ic_launcher-web.png
new file mode 100644
index 00000000..c4294efe
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/ic_launcher-web.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ByteArrayFrameBuilder.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ByteArrayFrameBuilder.java
new file mode 100644
index 00000000..58c6d7b5
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ByteArrayFrameBuilder.java
@@ -0,0 +1,48 @@
+package com.google.widevine.fulldecryptpathtesting;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * byte[]-based test frame builder
+ */
+
+public class ByteArrayFrameBuilder implements ITestFrameBuilder {
+ private final int mSize;
+ private final ByteArrayOutputStream mBao;
+
+ public ByteArrayFrameBuilder(int size) {
+ mBao = new ByteArrayOutputStream(size);
+ mSize = size;
+ }
+
+ @Override
+ public int capacity() {
+ return mSize;
+ }
+
+ @Override
+ public void accept(byte[] bytes) {
+ try {
+ mBao.write(bytes);
+ } catch (IOException e) {
+ throw new RuntimeException("shouldn't happen", e);
+ }
+ }
+
+ @Override
+ public void accept(ByteBuffer bytes) {
+ throw new UnsupportedOperationException(bytes.toString());
+ }
+
+ @Override
+ public byte[] build() {
+ return mBao.toByteArray();
+ }
+
+ @Override
+ public ByteBuffer buildBuf() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ByteBufferFrameBuffer.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ByteBufferFrameBuffer.java
new file mode 100644
index 00000000..0c4caf3c
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ByteBufferFrameBuffer.java
@@ -0,0 +1,40 @@
+package com.google.widevine.fulldecryptpathtesting;
+
+import java.nio.ByteBuffer;
+
+/**
+ * ByteBuffer-based test frame builder
+ */
+
+public class ByteBufferFrameBuffer implements ITestFrameBuilder {
+ private final ByteBuffer mBuf;
+
+ public ByteBufferFrameBuffer(ByteBuffer buf) {
+ this.mBuf = buf;
+ }
+
+ @Override
+ public int capacity() {
+ return mBuf.capacity();
+ }
+
+ @Override
+ public void accept(byte[] bytes) {
+ mBuf.put(bytes);
+ }
+
+ @Override
+ public void accept(ByteBuffer bytes) {
+ mBuf.put(bytes);
+ }
+
+ @Override
+ public byte[] build() {
+ return mBuf.hasArray() ? mBuf.array() : null;
+ }
+
+ @Override
+ public ByteBuffer buildBuf() {
+ return mBuf;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/CodecHandler.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/CodecHandler.java
new file mode 100644
index 00000000..a6f4f038
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/CodecHandler.java
@@ -0,0 +1,191 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.media.MediaCodec;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceView;
+import java.nio.ByteBuffer;
+
+/** Creates a codec and deals with sending data down to the codec and the widevine plugin. */
+public class CodecHandler {
+ private static final String TAG = "FDPT_CodecHandler";
+
+ private Logger mLogger;
+ private SurfaceView mView;
+
+ private MediaCrypto mCrypto;
+ private MediaCodec mCodec = null;
+
+ private ByteBuffer mInputBuffer;
+ private ByteBuffer mOutputBuffer;
+ private long mPresentationTime = -1;
+ private MediaFormat mCurrentFormat;
+ private final IWorker mWorker;
+
+ private static final int LONG_TIME_OUT_US = 2 * 1000 * 1000; // 2 second timeout.
+ private static final int SHORT_TIME_OUT_US = 2 * 1000; // 2 ms timeout.
+ private static final int FRAME_RATE = 25; // frames / second.
+ private static final int FRAME_DELTA = 1000 * 1000 / FRAME_RATE; // usec between frames.
+
+ public static final String MIME_TYPE = "video/avc";
+
+ CodecHandler(
+ Logger logger,
+ MediaCrypto crypto,
+ SurfaceView view,
+ MediaFormat format,
+ IWorker worker) {
+ mLogger = logger;
+ mCrypto = crypto;
+ mView = view;
+ mCurrentFormat = format;
+ mWorker = worker;
+ }
+
+ void createCodecByType(boolean mUseSecureBuffer) {
+ Log.d(TAG, "createCodecByType secure =" + mUseSecureBuffer);
+ try {
+ mCodec = MediaCodec.createDecoderByType(MIME_TYPE);
+ if (mUseSecureBuffer) {
+ String defaultName = mCodec.getName();
+ String secureName = defaultName + ".secure";
+ mCodec = MediaCodec.createByCodecName(secureName);
+ }
+ } catch (Exception e) {
+ mCodec = null;
+ mWorker.handleException("codec creation", e);
+ return;
+ }
+ mLogger.logInfo("Using codec " + mCodec.getName(), true);
+ }
+
+ void createCodecByName(String name) {
+ Log.d(TAG, "createCodecByName name =" + name);
+ try {
+ mCodec = MediaCodec.createByCodecName(name);
+ } catch (Exception e) {
+ mCodec = null;
+ mWorker.handleException("codec creation named " + name, e);
+ return;
+ }
+ mLogger.logInfo("Using codec " + mCodec.getName(), true);
+ }
+
+ boolean start() {
+ if (mCodec == null) return false;
+ if (mCrypto == null) return false;
+ Surface surface = mView.getHolder().getSurface();
+ Log.d(TAG, "using surface " + surface);
+ // Note: when tested, the format is width=1280, height=720,
+ // durationUs is set -- maybe we need to make it a larger number?
+ // XXX-- this is probably wrong. We want to add padding to that.
+ // max-input-size is set to 124410. We might want to increase this to be
+ // the maximum buffer size we test?
+ // frame-count = 411. maybe we should change this?
+ // mime = "video/avc", which agrees with mimeType.
+ // level = 512. what is this?
+ // frame-rate = 18. ok?
+
+ // TODO: get max size should be known by test case manager.
+ // mCurrentFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 200000);
+
+ try {
+ mCodec.configure(mCurrentFormat, surface, mCrypto, 0 /* decode */);
+ mCodec.start();
+ } catch (Exception ex) {
+ mWorker.handleException("starting codec.", ex);
+ return false;
+ }
+ return true;
+ }
+
+ boolean sendData(TestCase test, byte[] clearFrame, byte[] encryptedFrame, boolean lastFrame) {
+ try {
+ if (mCodec == null) return false;
+
+ int bufferIndex = mCodec.dequeueInputBuffer(LONG_TIME_OUT_US);
+ if (bufferIndex == -1) {
+ mLogger.setError("Could not get input buffer.");
+ return false;
+ }
+
+ mInputBuffer = mCodec.getInputBuffer(bufferIndex);
+ mInputBuffer.put(ByteBuffer.wrap(encryptedFrame));
+
+ int flags = 0;
+ // TODO: is this needed if we call flush in finishFrames below?
+ if (lastFrame) {
+ flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ }
+ // TODO: how heavily is this inforced? Should we get it from the clock?
+ long presentationTimeUs = FRAME_DELTA * test.getFrameNumber();
+ mCodec.queueSecureInputBuffer(
+ bufferIndex, 0 /* offset */, test.getCryptoInfo(), presentationTimeUs, flags);
+ processOutput();
+
+ } catch (Exception ex) {
+ mWorker.handleException("Codec issue.", ex);
+ return false;
+ }
+ return true;
+ }
+
+ private void processOutput() {
+ try {
+ MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+ while (true) {
+ int outputBufferIndex = mCodec.dequeueOutputBuffer(info, SHORT_TIME_OUT_US);
+ if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ return;
+ } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ mCurrentFormat = mCodec.getOutputFormat();
+ return;
+ } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+ return;
+ } else if (outputBufferIndex >= 0) {
+ mCodec.releaseOutputBuffer(outputBufferIndex, true);
+ }
+ }
+ } catch (Exception ex) {
+ mWorker.handleException("process output.", ex);
+ }
+ }
+
+ void finishFrames() throws InterruptedException {
+ Log.d(TAG, "finish frames");
+ if (mCodec == null) return;
+ // TODO: clean this up. It should flush all the buffers, but it down't
+ // need to do much else.
+ Thread.sleep(2000); // TODO: probably not needed.
+ Log.d(TAG, "Process output in finishFrames.");
+ processOutput();
+ Log.d(TAG, "calling flush.");
+ mCodec.flush();
+ Log.d(TAG, "Another processOutput.");
+ processOutput();
+ cleanUp();
+ Log.d(TAG, "done with finish frames");
+ }
+
+ // This can be called multiple times.
+ void cleanUp() {
+ try {
+ Log.d(TAG, "cleanup");
+ if (mCodec == null) return;
+ Log.d(TAG, "codec stop.");
+ mCodec.stop();
+ Log.d(TAG, "codec release.");
+ mCodec.release();
+ mCodec = null;
+ } catch (Exception ex) {
+ mWorker.handleException("codec cleanup.", ex);
+ }
+ Log.d(TAG, "done with cleanup");
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/CodecPreference.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/CodecPreference.java
new file mode 100644
index 00000000..222c920c
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/CodecPreference.java
@@ -0,0 +1,58 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.content.Context;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+import java.util.ArrayList;
+
+/**
+ * This is a UI element on the settings/preference screen. It generates a list of codecs from which
+ * the user can choose.
+ */
+public class CodecPreference extends ListPreference {
+ private static final String TAG = "FDPT_CodecPreference";
+
+ public CodecPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ ArrayList entries = new ArrayList();
+ ArrayList values = new ArrayList();
+
+ MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
+ MediaCodecInfo[] codecInfos = list.getCodecInfos();
+ values.add(context.getString(R.string.default_codec));
+ entries.add(context.getString(R.string.default_codec_description));
+ for (MediaCodecInfo info : codecInfos) {
+ if (!info.isEncoder()) {
+ boolean include = false;
+ StringBuilder sb = new StringBuilder();
+ sb.append(info.getName() + ": ");
+ String[] supportedTypes = info.getSupportedTypes();
+ for (String string : supportedTypes) {
+ if (CodecHandler.MIME_TYPE.equals(string)) include = true;
+ sb.append(" " + string);
+ }
+ if (include) {
+ values.add(info.getName());
+ entries.add(sb.toString());
+ Log.d(TAG, sb.toString());
+ }
+ }
+ }
+ CharSequence[] array = new CharSequence[entries.size()];
+ setEntries(entries.toArray(array));
+ setEntryValues(values.toArray(array));
+ // TODO: pick best default based on secure or not secure,
+ // TODO: set value based on current preference value.
+ }
+
+ public CodecPreference(Context context) {
+ this(context, null);
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Encryptor.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Encryptor.java
new file mode 100644
index 00000000..230dac27
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Encryptor.java
@@ -0,0 +1,404 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.media.MediaCodec;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Encrypts frames based on the mode and pattern in the test case. */
+public class Encryptor {
+ private static final String TAG = "FDPT_Encryptor";
+
+ private LicenseHolder mLicenseHolder;
+ private Logger mLogger;
+
+ static final int AES_BLOCK_SIZE = 16;
+
+ Encryptor(LicenseHolder licenseHolder, Logger logger) {
+ this.mLicenseHolder = licenseHolder;
+ this.mLogger = logger;
+ }
+
+ byte[] encryptFrame(byte[] frame, TestCase test) {
+ switch (test.getCryptoInfo().mode) {
+ default:
+ mLogger.setError("Unsupported crypto mode: " + test.getCryptoInfo().mode);
+ return frame;
+ case MediaCodec.CRYPTO_MODE_UNENCRYPTED:
+ return frame;
+ case MediaCodec.CRYPTO_MODE_AES_CTR:
+ if (test.getEncryptBlocks() > 0) {
+ return encryptCTRWithPattern(frame, test);
+ } else {
+ return encryptCTRNoSkipPattern(frame, test);
+ }
+ case MediaCodec.CRYPTO_MODE_AES_CBC:
+ if (test.getEncryptBlocks() > 0) {
+ return encryptCBCWithPattern(frame, test);
+ } else {
+ return encryptCBCNoSkipPattern(frame, test);
+ }
+ }
+ }
+
+ // Extract the encrypted subsamples into one single buffer. This is used for
+ // all but "cbcs" mode.
+ byte[] extractEncryptedSubsamples(final byte[] frame, TestCase test) {
+ MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
+ int numEncryptedBytes = 0;
+ int numSubSamples = cryptoInfo.numSubSamples;
+ for (int i = 0; i < numSubSamples; ++i) {
+ numEncryptedBytes += cryptoInfo.numBytesOfEncryptedData[i];
+ }
+ byte[] output = new byte[numEncryptedBytes];
+ int inputOffset = 0;
+ int outputOffset = 0;
+ for (int i = 0; i < numSubSamples; ++i) {
+ int numClear = cryptoInfo.numBytesOfClearData[i];
+ int numEncrypt = cryptoInfo.numBytesOfEncryptedData[i];
+ inputOffset += numClear;
+ System.arraycopy(frame, inputOffset, output, outputOffset, numEncrypt);
+ inputOffset += numEncrypt;
+ outputOffset += numEncrypt;
+ }
+ return output;
+ }
+
+ // In the function above, we pulled out the encrypted subsamples. In this function
+ // we re-assemble the subsamples after the encrypting the single buffer.
+ byte[] combineSubsamples(
+ final byte[] frame, // original
+ final byte[] encrypted,
+ TestCase test) {
+ MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
+ byte[] output = new byte[frame.length];
+ int numSubSamples = cryptoInfo.numSubSamples;
+ int frameOffset = 0;
+ int encryptOffset = 0;
+ for (int i = 0; i < numSubSamples; ++i) {
+ int numClear = cryptoInfo.numBytesOfClearData[i];
+ int numEncrypt = cryptoInfo.numBytesOfEncryptedData[i];
+ System.arraycopy(frame, frameOffset, output, frameOffset, numClear);
+ frameOffset += numClear;
+ System.arraycopy(encrypted, encryptOffset, output, frameOffset, numEncrypt);
+ frameOffset += numEncrypt;
+ encryptOffset += numEncrypt;
+ }
+ return output;
+ }
+
+ // This takes an IV and resets the low order bytes to 0. This is needed to manually wrap
+ // the iv on overflow.
+ byte[] resetIV(byte[] oldIV) {
+ byte[] iv = new byte[AES_BLOCK_SIZE];
+ for (int i = 0; i < 8; i++) {
+ iv[i] = oldIV[i];
+ }
+ for (int i = 8; i < 16; i++) {
+ iv[i] = 0;
+ }
+ return iv;
+ }
+
+ // 'CENC' protection scheme. This is counter mode with no pattern. We treat the encrypted
+ // subsamples as one long block in order to chain the iv.
+ byte[] encryptCTRNoSkipPattern(final byte[] frame, TestCase test) {
+ try {
+ byte[] tempBuffer = extractEncryptedSubsamples(frame, test);
+ byte[] encrypted = new byte[tempBuffer.length];
+
+ final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
+ SecretKey key = new SecretKeySpec(mLicenseHolder.getTestKey(), "AES");
+
+ MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
+
+ int bytesEncrypted = 0;
+ int bytesToEncrypt = encrypted.length;
+ int bytesToEncryptAfterWrap = 0;
+
+ // First, check to see if the iv will overflow/wrap when we increment it. For the CENC
+ // standard, we increment the low order 8 bytes as a 64 bit unsigned integer. The
+ // javax.crypto.Cipher will try to increment the iv as a 128 bit unsigned integer. To
+ // prevent this, we check to see if the IV would wrap, and break the buffer into two
+ // chunks if it would wrap. Convert the second batch of bytes to a 64 bit integer
+ // counter.
+ byte[] iv = cryptoInfo.iv;
+ long counter = 0;
+ for (int i = 8; i < 16; i++) {
+ counter = (counter << 8) + (iv[i] & 0xFFL);
+ }
+ // The 64 bit counter is a signed integer, because that's how Java works. If we pretend
+ // it is an unsigned integer, then addition overflows or wraps when the high order bit
+ // changes from 1 to 0. Since Java is doing signed addition, that means |counter| is
+ // negative, and |endCounter| is positive.
+ long endCounter = counter + (bytesToEncrypt / AES_BLOCK_SIZE);
+ if (counter < 0 && endCounter > 0) {
+ // The first batch to encrypt is -counter -- i.e. how far from the overflow point.
+ int firstBatch = (int) (-counter);
+ bytesToEncryptAfterWrap = bytesToEncrypt - firstBatch * AES_BLOCK_SIZE;
+ bytesToEncrypt = firstBatch * AES_BLOCK_SIZE;
+ }
+
+ // Loop at most twice to encrypt before the wrap and after the wrap.
+ while (bytesToEncrypt > 0) {
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+ int result =
+ cipher.update(
+ tempBuffer,
+ bytesEncrypted,
+ bytesToEncrypt,
+ encrypted,
+ bytesEncrypted);
+ if (bytesToEncrypt != result) {
+ mLogger.setError(
+ "encryptCTRNoSkipPattern error: expected="
+ + bytesToEncrypt
+ + " , actual="
+ + result);
+ }
+ cipher.doFinal();
+
+ // Increment counters. If there was a wrap, we have to loop back.
+ bytesEncrypted += bytesToEncrypt;
+ bytesToEncrypt = bytesToEncryptAfterWrap;
+ bytesToEncryptAfterWrap = 0; // Ensure we loop at most twice.
+ iv = resetIV(cryptoInfo.iv);
+ }
+ return combineSubsamples(frame, encrypted, test);
+ } catch (Exception ex) {
+ mLogger.logException("Encrypt CTR error: ", ex);
+ }
+ return frame;
+ }
+
+ // 'cbc1' protection scheme. This uses CBC with no pattern. We can combine all the
+ // subsamples and let the normal CBC algorithm chain the IVs. We may also assume that the
+ // encrypted subsamples end on an AES block boundary.
+ byte[] encryptCBCNoSkipPattern(final byte[] frame, TestCase test) {
+ try {
+ final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKey key = new SecretKeySpec(mLicenseHolder.getTestKey(), "AES");
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(test.getCryptoInfo().iv));
+
+ byte[] tempBuffer = extractEncryptedSubsamples(frame, test);
+ byte[] encrypted = new byte[tempBuffer.length];
+
+ int bytesToEncrypt = encrypted.length;
+ // If a sample does not end on a block boundary, we just copy the
+ // remainder as unencrypted bytes.
+ if (bytesToEncrypt % AES_BLOCK_SIZE != 0) {
+ int remainder = bytesToEncrypt % AES_BLOCK_SIZE;
+ bytesToEncrypt -= remainder;
+ System.arraycopy(tempBuffer, bytesToEncrypt, encrypted, bytesToEncrypt, remainder);
+ }
+
+ int result = cipher.update(tempBuffer, 0, bytesToEncrypt, encrypted, 0);
+ cipher.doFinal();
+ if (result != bytesToEncrypt) {
+ mLogger.setError(
+ "encryptCBCNoSkipPattern error: expected="
+ + bytesToEncrypt
+ + " , actual="
+ + result);
+ }
+ return combineSubsamples(frame, encrypted, test);
+ } catch (Exception ex) {
+ mLogger.logException("Encrypt CBC error: ", ex);
+ }
+ return frame;
+ }
+
+ // 'cens' protection scheme. This is CTR with a pattern. All of the test cases that
+ // we need to support assume that the encrypted subsamples end on an AES block boundary.
+ byte[] encryptCTRWithPattern(final byte[] frame, TestCase test) {
+ try {
+ final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
+ SecretKey key = new SecretKeySpec(mLicenseHolder.getTestKey(), "AES");
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(test.getCryptoInfo().iv));
+
+ byte[] output = new byte[frame.length];
+ MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
+ int numSubSamples = cryptoInfo.numSubSamples;
+
+ // This is similar to the code above in encryptCTRNoSkipPattern(), except we increment
+ // the counter/iv after each pattern.
+ byte[] iv = cryptoInfo.iv;
+ long counter = 0;
+ for (int i = 8; i < 16; i++) {
+ counter = (counter << 8) + (iv[i] & 0xFFL);
+ }
+
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+ int offset = 0;
+ for (int i = 0; i < numSubSamples; ++i) {
+ // clear bytes come first for ISO common encryption
+ int numClearBytes = cryptoInfo.numBytesOfClearData[i];
+ if (numClearBytes > 0) {
+ System.arraycopy(frame, offset, output, offset, numClearBytes);
+ offset += numClearBytes;
+ }
+
+ int numEncryptedBytes = cryptoInfo.numBytesOfEncryptedData[i];
+ if (numEncryptedBytes > 0) {
+ int bytesRemaining = numEncryptedBytes;
+ while (bytesRemaining > 0) {
+ int bytesToEncrypt = test.getEncryptBlocks() * AES_BLOCK_SIZE;
+ int bytesToSkip =
+ Math.min(
+ test.getSkipBlocks() * AES_BLOCK_SIZE,
+ bytesRemaining - bytesToEncrypt);
+ if (bytesRemaining < bytesToEncrypt) {
+ // The "cens" spec says that "any partial crypt_byte_block SHALL remain
+ // unencrypted." We are interpreting this to mean that if the data does
+ // not have enough bytes to fill all of the blocks in the encrypted part
+ // of the pattern, then none of the bytes are encrypted.
+ bytesToEncrypt = 0;
+ bytesToSkip = bytesRemaining;
+ }
+
+ // Check to see if the iv will wrap on this chunk. See the comments in
+ // encryptCTRNoSkipPattern() about counter overflow.
+ long newCounter = counter + (bytesToEncrypt / AES_BLOCK_SIZE);
+ int remainder = 0;
+ boolean overflow = false;
+ if (counter < 0 && newCounter >= 0) {
+ overflow = true;
+ int firstBatch = (int) (-counter);
+ remainder = bytesToEncrypt - firstBatch * AES_BLOCK_SIZE;
+ bytesToEncrypt = firstBatch * AES_BLOCK_SIZE;
+ }
+ int bytesEncrypted =
+ cipher.update(frame, offset, bytesToEncrypt, output, offset);
+ if (bytesEncrypted != bytesToEncrypt) {
+ mLogger.setError(
+ "encryptCTRWithPattern error: expected="
+ + bytesToEncrypt
+ + " , actual="
+ + bytesEncrypted);
+ }
+ offset += bytesToEncrypt;
+ bytesRemaining -= bytesToEncrypt;
+ counter = newCounter;
+ if (overflow) {
+ cipher.doFinal();
+ iv = resetIV(cryptoInfo.iv);
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+ if (remainder > 0) {
+ int result =
+ cipher.update(frame, offset, remainder, output, offset);
+ if (result != remainder) {
+ mLogger.setError(
+ "encryptCTRWithPattern error: expected="
+ + remainder
+ + " , actual="
+ + result);
+ }
+ offset += remainder;
+ bytesRemaining -= remainder;
+ }
+ }
+ System.arraycopy(frame, offset, output, offset, bytesToSkip);
+ offset += bytesToSkip;
+ bytesRemaining -= bytesToSkip;
+ }
+ }
+ }
+ if (offset != output.length) {
+ mLogger.setError("cens buffer size: " + output.length + " , actual=" + offset);
+ }
+ cipher.doFinal();
+ return output;
+ } catch (Exception ex) {
+ mLogger.logException("Encrypt CTR error: ", ex);
+ }
+ return frame;
+ }
+
+ // 'cbcs' protection scheme. This is used for HLS. This is CBC with a pattern.
+ // Unlike the other three schemes, the IV is reset at the start of each subsample.
+ // If the encrypted subsample does not end on an AES block boundary, we just copy the
+ // partial remaining block.
+ byte[] encryptCBCWithPattern(final byte[] frame, TestCase test) {
+ try {
+ final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKey key = new SecretKeySpec(mLicenseHolder.getTestKey(), "AES");
+
+ byte[] output = new byte[frame.length];
+ MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
+ int numSubSamples = cryptoInfo.numSubSamples;
+ int offset = 0;
+
+ // If input length does not align on AES_BLOCK_SIZE,
+ // we encrypt up to the partial block and leave the partial
+ // block in the clear.
+ for (int i = 0; i < numSubSamples; ++i) {
+ // clear bytes come first for ISO common encryption
+ int numClearBytes = cryptoInfo.numBytesOfClearData[i];
+ if (numClearBytes > 0) {
+ System.arraycopy(frame, offset, output, offset, numClearBytes);
+ offset += numClearBytes;
+ }
+
+ // How many bytes left in this subsample...
+ int bytesRemaining = cryptoInfo.numBytesOfEncryptedData[i];
+ // Except we will handle a final partial block separately, because
+ // it is never encrypted.
+ int partialBlock = bytesRemaining % AES_BLOCK_SIZE;
+ // So, subtract the partial block.
+ bytesRemaining -= partialBlock;
+ if (bytesRemaining > 0) {
+ // resets the IV for each subsample
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(cryptoInfo.iv));
+ while (bytesRemaining > 0) {
+ int bytesToEncrypt = test.getEncryptBlocks() * AES_BLOCK_SIZE;
+ int bytesToSkip =
+ Math.min(
+ test.getSkipBlocks() * AES_BLOCK_SIZE,
+ bytesRemaining - bytesToEncrypt);
+ if (bytesRemaining < bytesToEncrypt) {
+ // The "cbcs" spec says that "any partial crypt_byte_block SHALL remain
+ // unencrypted." We are interpreting this to mean that if the data does
+ // not have enough bytes to fill all of the blocks in the encrypted part
+ // of the pattern, then none of the bytes are encrypted.
+ bytesToEncrypt = 0;
+ bytesToSkip = bytesRemaining;
+ }
+ if (bytesToEncrypt > 0) {
+ int bytesEncrypted =
+ cipher.update(frame, offset, bytesToEncrypt, output, offset);
+ if (bytesToEncrypt != bytesEncrypted) {
+ mLogger.setError(
+ "encryptCBCWithPattern error: expected="
+ + bytesToEncrypt
+ + " , actual="
+ + bytesEncrypted);
+ }
+ offset += bytesEncrypted;
+ bytesRemaining -= bytesEncrypted;
+ }
+ if (bytesToSkip > 0) {
+ System.arraycopy(frame, offset, output, offset, bytesToSkip);
+ offset += bytesToSkip;
+ bytesRemaining -= bytesToSkip;
+ }
+ }
+ cipher.doFinal();
+ }
+ if (partialBlock > 0) {
+ // do not encrypt bytes that are less than block size
+ System.arraycopy(frame, offset, output, offset, partialBlock);
+ offset += partialBlock;
+ }
+ }
+ return output;
+ } catch (Exception ex) {
+ mLogger.logException("Encrypt CBC error: ", ex);
+ }
+ return frame;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/FDPTApplication.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/FDPTApplication.java
new file mode 100644
index 00000000..d1c70b05
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/FDPTApplication.java
@@ -0,0 +1,54 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.app.Application;
+
+public class FDPTApplication extends Application {
+
+ static final String TAG = "FDPT";
+
+ // Used to load the 'native-lib' library on application startup.
+ static {
+ System.loadLibrary("native-lib");
+ }
+
+ private Logger mLogger;
+ private Worker mWorker;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mLogger = new Logger();
+ mWorker = new Worker(mLogger);
+ mWorker.start();
+ }
+
+ @Override
+ public void onTerminate() {
+ super.onTerminate();
+ mWorker.onStop();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ }
+
+ synchronized void setActivity(MainActivity activity) {
+ mWorker.onPause();
+ mLogger.setActivity(activity);
+ // TODO: tear down codec's surface and recreate it. This is only
+ // needed if we restart the activity while a test is already running.
+ }
+
+ Logger getLogger() {
+ return mLogger;
+ }
+
+ Worker getWorker() {
+ return mWorker;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/FrameGenerator.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/FrameGenerator.java
new file mode 100644
index 00000000..a2067aea
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/FrameGenerator.java
@@ -0,0 +1,184 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/** Generates a valid video key frame. */
+public class FrameGenerator {
+ private static final String TAG = "FDPT_FrameGenerator";
+
+ private final int AES_BLOCK_SIZE = 16;
+ private final int NALU_4_BYTES_START_CODE = 0x0001;
+ private final int NALU_HEADER_SIZE = 5; // 4 bytes start code + 1 byte type
+ private final int SEI_MESSAGE_HEADER_SIZE = 2; // 1 byte type and 1 byte size
+ private final int MAX_SEI_PAYLOAD_SIZE = 255;
+ private final int MIN_HEXDUMP_BYTES = 32;
+
+ // smallest SEI message has 1 byte payload
+ private final int MIN_NALU_SIZE = NALU_HEADER_SIZE + SEI_MESSAGE_HEADER_SIZE + 1;
+
+ private Context mContext;
+ private Logger mLogger;
+ private MediaFormat mMediaFormat;
+ private String mMimeType;
+ private byte[] mKeyFrame;
+ private int mKeyFrameSize = 0;
+
+ private int getMediaFormatInteger(MediaFormat format, String key) {
+ return format.containsKey(key) ? format.getInteger(key) : 0;
+ }
+
+ FrameGenerator(Context context, Logger logger) {
+ this.mContext = context;
+ this.mLogger = logger;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.P)
+ void prepareKeyFrame() {
+ MediaExtractor extractor = new MediaExtractor();
+ mKeyFrameSize = 0;
+
+ try (AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(R.raw.fdpt)) {
+ extractor.setDataSource(afd);
+ int tracks = extractor.getTrackCount();
+ for (int i = 0; i < tracks; ++i) {
+ MediaFormat mediaFormat = extractor.getTrackFormat(i);
+ String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
+ if (mimeType.startsWith("video/")) {
+ mLogger.logInfo("track #" + i + " " + mediaFormat, true);
+ mLogger.logInfo(
+ "Width:"
+ + getMediaFormatInteger(mediaFormat, MediaFormat.KEY_WIDTH)
+ + " Height:"
+ + getMediaFormatInteger(mediaFormat, MediaFormat.KEY_HEIGHT)
+ + " "
+ + mimeType,
+ true);
+ extractor.selectTrack(i);
+ }
+ }
+
+ do {
+ long sampleSize = extractor.getSampleSize();
+ if (sampleSize == -1) {
+ break;
+ } else if ((extractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
+ ByteBuffer inputBuffer = ByteBuffer.allocate((int) sampleSize);
+ mKeyFrameSize = extractor.readSampleData(inputBuffer, 0 /*offset*/);
+ mKeyFrame = new byte[mKeyFrameSize];
+ inputBuffer.get(mKeyFrame, 0, mKeyFrameSize);
+
+ mMediaFormat = extractor.getTrackFormat(extractor.getSampleTrackIndex());
+ mMimeType = mMediaFormat.getString(MediaFormat.KEY_MIME);
+ break;
+ }
+ } while (extractor.advance());
+
+ } catch (IOException ioex) {
+ mLogger.setError("Could not open video source, exception=" + ioex);
+ } finally {
+ extractor.release();
+ }
+ }
+
+ byte[] buildtestFrame(ITestFrameBuilder builder) {
+ return buildtestFrame(builder, builder.capacity());
+ }
+
+ // Appends one or more NALU (tail NAL units) at the end of the key frame.
+ // The NALU is of type 6: Supplemental enhancement information (SEI)
+ // and will be ignored by the decoder.
+ byte[] buildtestFrame(ITestFrameBuilder builder, int requestSize) {
+ final int SEI_MESSAGE_TYPE = 0x66;
+ final int UNREGISTERED_USER_DATA = 5;
+ final byte[] DEAD_FEED = new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xFE, (byte) 0xED};
+
+ int bytesToFill = requestSize - mKeyFrameSize;
+ int naluSize = MAX_SEI_PAYLOAD_SIZE + NALU_HEADER_SIZE + SEI_MESSAGE_HEADER_SIZE;
+ final int numNaluNeeded = bytesToFill / naluSize + (bytesToFill % naluSize == 0 ? 0 : 1);
+
+ int totalPayloadSize =
+ bytesToFill - (NALU_HEADER_SIZE + SEI_MESSAGE_HEADER_SIZE) * numNaluNeeded;
+
+ builder.accept(mKeyFrame);
+ for (int j = 0; j < numNaluNeeded; ++j) {
+ int payloadSize = Math.min(MAX_SEI_PAYLOAD_SIZE, totalPayloadSize);
+ naluSize = NALU_HEADER_SIZE + SEI_MESSAGE_HEADER_SIZE + payloadSize;
+ totalPayloadSize -= payloadSize;
+
+ // 0x66 (0110 0110) format:
+ // forbidden_zero_bit (1 bit) = 0
+ // nal_ref_idc (2 bits) = 3
+ // nal_unit_type (5 bits) = 6 (SEI message)
+ // SEI message {
+ // payload type (1 byte) 5 (user data unregistered)
+ // payload size (1 byte)
+ // actual message
+ // }
+ ByteBuffer nalu = ByteBuffer.allocate(naluSize);
+ nalu.putInt(NALU_4_BYTES_START_CODE);
+ nalu.put((byte) SEI_MESSAGE_TYPE);
+ nalu.put((byte) UNREGISTERED_USER_DATA);
+ nalu.put((byte) payloadSize);
+
+ for (int i = 0; i < payloadSize; ++i) {
+ nalu.put(DEAD_FEED[i % DEAD_FEED.length]);
+ }
+
+ builder.accept(nalu.array());
+
+ dumpTailNalu(naluSize, nalu, false /* verbose */);
+ }
+ return builder.build();
+ }
+
+ private void dumpTailNalu(int naluSize, ByteBuffer nalu, boolean verbose) {
+ if (verbose == false) return;
+
+ mLogger.logInfo("tail NALU size=" + naluSize, true);
+ mLogger.logInfo(
+ "tail NALU:\n"
+ + HexUtil.dumpPrettyHexBytes(
+ nalu.array(), 0, Math.min(MIN_HEXDUMP_BYTES, naluSize)),
+ true);
+ }
+
+ private void dumpPartialFrame(byte[] testFrame, int naluSize, boolean verbose) {
+ if (verbose == false) return;
+
+ mLogger.logInfo(
+ "test frame head:\n" + HexUtil.dumpPrettyHexBytes(testFrame, 0, MIN_HEXDUMP_BYTES),
+ true);
+ mLogger.logInfo(
+ "test frame tail:\n"
+ + HexUtil.dumpPrettyHexBytes(
+ testFrame,
+ testFrame.length - naluSize,
+ Math.min(MIN_HEXDUMP_BYTES, naluSize)),
+ true);
+ }
+
+ // TODO: is this needed?
+ final String getMimeType() {
+ return mMimeType;
+ }
+
+ // The minimum frame size is keyFrame plus a smallest NALU for padding.
+ final int getMinimumFrameSize() {
+ return mKeyFrame.length + MIN_NALU_SIZE;
+ }
+
+ final MediaFormat getMediaFormat() {
+ return mMediaFormat;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/HashGenerator.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/HashGenerator.java
new file mode 100644
index 00000000..d914b574
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/HashGenerator.java
@@ -0,0 +1,25 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+/**
+ * Computes a hash of each frame. This version of HashGenerator uses native C++ code to compute a
+ * CRC of the frame. Vendors who wish to use a different type of hash or checksum should modify this
+ * class. You can either write your code in Java, or look in the directory mobile/src/main/cpp and
+ * modify the native-lib.cpp file. Make sure you update mobile/CMakeLists.txt if you add any new C++
+ * source files.
+ */
+public class HashGenerator {
+ HashGenerator() {}
+
+ // Compute the hash of one frame.
+ byte[] getHash(byte[] frame, TestCase test) {
+ byte[] hash = new byte[4]; // A CRC 32 is 4 bytes long.
+ computeCRC(frame, hash);
+ return hash;
+ }
+
+ public native void computeCRC(byte[] frame, byte[] hash);
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/HexUtil.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/HexUtil.java
new file mode 100644
index 00000000..7822d80a
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/HexUtil.java
@@ -0,0 +1,69 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+/**
+ * Dump buffer in hex, 16 bytes per row.
+ *
+ * @param inputBuffer buffer to dump
+ * @param offset index to the first byte to dump
+ * @param length length of buffer
+ */
+final class HexUtil {
+ static String dumpPrettyHexBytes(byte[] inputBuffer, int offset, int length) {
+ final int NUM_COLUMNS = 16;
+
+ StringBuilder builder = new StringBuilder();
+ for (int row = offset; row < offset + length; row += NUM_COLUMNS) {
+ builder.append(String.format("%06d: ", row));
+
+ for (int col = 0; col < NUM_COLUMNS; ++col) {
+ if (row + col < inputBuffer.length) {
+ builder.append(String.format("%02x ", inputBuffer[row + col]));
+ } else {
+ break;
+ }
+ }
+ builder.append(String.format("\n"));
+ }
+
+ return builder.toString();
+ }
+
+ static String dumpHexBytes(byte[] inputBuffer, int offset, int length) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = offset; i < length; i++) {
+ builder.append(String.format("%02x", inputBuffer[i]));
+ }
+ return builder.toString();
+ }
+
+ private static byte charsToByte(char left, char right) {
+ final int leftByte = Character.digit(left, 16) << 4;
+ final int rightByte = Character.digit(right, 16);
+
+ return (byte) ((leftByte | rightByte) & 0xFF);
+ }
+
+ public static byte[] fromString(String s) {
+ final int length = s.length();
+ final byte[] data = new byte[length / 2];
+
+ for (int i = 0; i < length; i += 2) {
+ data[i / 2] = charsToByte(s.charAt(i), s.charAt(i + 1));
+ }
+
+ return data;
+ }
+
+ public static String toString(byte[] array) {
+ StringBuilder sb = new StringBuilder();
+
+ for (byte b : array) {
+ sb.append(String.format("%02X", b));
+ }
+ return sb.toString();
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ITestFrameBuilder.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ITestFrameBuilder.java
new file mode 100644
index 00000000..1363e1e6
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/ITestFrameBuilder.java
@@ -0,0 +1,15 @@
+package com.google.widevine.fulldecryptpathtesting;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Interface to build test frame from a key frame and trailing nal units
+ */
+
+public interface ITestFrameBuilder {
+ int capacity();
+ void accept(byte[] bytes);
+ void accept(ByteBuffer bytes);
+ byte[] build();
+ ByteBuffer buildBuf();
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/IWorker.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/IWorker.java
new file mode 100644
index 00000000..d37a1bf8
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/IWorker.java
@@ -0,0 +1,15 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+public interface IWorker {
+ void maybePause();
+
+ boolean finishEarly();
+
+ void handleException(String message, Exception e);
+
+ void handleError(String message);
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/LicenseHolder.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/LicenseHolder.java
new file mode 100644
index 00000000..9226485d
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/LicenseHolder.java
@@ -0,0 +1,335 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.content.Context;
+import android.media.DeniedByServerException;
+import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.media.ResourceBusyException;
+import android.media.UnsupportedSchemeException;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.UUID;
+
+/** Generates a widevine license. If the device needs to be provisioned, it also does that. */
+public class LicenseHolder {
+ private static final String TAG = "FDPT_LicenseHolder";
+
+ private Context mContext;
+ private Logger mLogger;
+ private boolean mUseLevel1;
+
+ private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
+ private static final String ORIGIN_KEY = "origin";
+ private static final String ORIGIN = "full-decrypt-path-testing";
+ private static final String mServerURL = "https://proxy.uat.widevine.com/proxy";
+ private static final String VIDEO_ID = "video_id=FDPT-test-contnt";
+
+ // A PSSH generated by the integration console, using the key id and content
+ // id below. This content id is valid on UAT and Widevine staging servers.
+ private final byte[] FDPT_PSSH =
+ HexUtil.fromString("2210464450542d746573742d636f6e746e7448e3dc959b06");
+
+ // Key ID for test content.
+ static final byte[] TEST_KEY_ID = "FDPT-test-key-id".getBytes();
+ // == 464450542d746573742d6b65792d694 in hex.
+
+ // The content id. Used by the license server.
+ private static final byte[] TEST_CONTENT_ID = "FDPT-test-contnt".getBytes();
+ // == 464450542d746573742d636f6e746e74 in hex.
+
+ // The key that is used to decrypt content.
+ private static final byte[] TEST_KEY_DATA = "0123456789abcdef".getBytes();
+ // == 30313233343536373839616263646566 in hex.
+
+ private MediaDrm mDrm = null;
+ private byte[] mSession;
+ private byte[] mKeySet;
+
+ private boolean mHasLicense = false;
+
+ LicenseHolder(Logger logger, boolean useLevel1, Context context) {
+ this.mContext = context;
+ this.mLogger = logger;
+ this.mUseLevel1 = useLevel1;
+ }
+
+ MediaCrypto getMediaCrypto() {
+ try {
+ if (!mHasLicense) return null;
+ return new MediaCrypto(WIDEVINE_UUID, mSession);
+ } catch (MediaCryptoException ex) {
+ mLogger.logException("Fetching MediaCrypto", ex);
+ return null;
+ }
+ }
+
+ void cleanUp() {
+ if (mDrm != null) {
+ try {
+ if (mSession != null) {
+ mDrm.closeSession(mSession);
+ mSession = null;
+ }
+ mDrm.close();
+ mDrm = null;
+ } catch (Exception ex) {
+ mLogger.logException("License cleanup", ex);
+ }
+ }
+ }
+
+ void init() {
+ try {
+ mDrm = new MediaDrm(WIDEVINE_UUID);
+ } catch (UnsupportedSchemeException ex) {
+ mLogger.logInfo("Widevine not supported: " + ex, true);
+ mLogger.setError("Widevine unsupported.");
+ return;
+ }
+ mDrm.setPropertyString(ORIGIN_KEY, ORIGIN);
+ if (!mUseLevel1) mDrm.setPropertyString("securityLevel", "L3");
+ dumpInfo();
+ }
+
+ boolean shouldSkipTest() {
+ if (mDrm == null) {
+ init();
+ if (mDrm == null) return true;
+ }
+ int oemCryptoVersion = -1;
+ String version = mDrm.getPropertyString("oemCryptoApiVersion");
+ try {
+ oemCryptoVersion = Integer.parseInt(version);
+ } catch (NumberFormatException e) {
+ mLogger.logException("OEMCrypto Format " + version, e);
+ return true;
+ }
+ if (oemCryptoVersion < 15) {
+ final CharSequence wrongApi =
+ "OEMCrypto v15 or greater is required,\n"
+ + "the device is reporting v"
+ + oemCryptoVersion
+ + ".\nSkip all tests.";
+ mLogger.setError(wrongApi.toString());
+ return true;
+ }
+
+ final String hashSupport = mDrm.getPropertyString("decryptHashSupport");
+ if (!"1".equals(hashSupport)) { // OEMCrypto_CRC_Clear_Buffer
+ final CharSequence noHash =
+ "Device returns OEMCrypto_Hash_Not_Supported.\n" + "Skip all tests.";
+ mLogger.setError(noHash.toString());
+ return true;
+ }
+ return false;
+ }
+
+ private void dumpInfo() {
+ mLogger.logInfo(android.os.Build.BRAND + " " + android.os.Build.BOARD, true);
+ dumpString(R.string.key_security_level, R.string.security_level);
+ dumpString(R.string.key_system_id, R.string.system_id);
+ dumpString(R.string.key_cdm_version, R.string.cdm_version);
+ dumpString(R.string.key_oemcrypto_api, R.string.oemcrypto_api);
+ dumpString(R.string.key_oemcrypto_build_info, R.string.oemcrypto_build_info);
+ dumpString(R.string.key_hash_support, R.string.hash_support);
+ dumpString(R.string.key_resource_rating, R.string.resource_rating);
+ }
+
+ private void dumpString(int key_index, int name_index) {
+ String name = mContext.getString(name_index);
+ String key = mContext.getString(key_index);
+ try {
+ mLogger.logInfo(name + ": " + mDrm.getPropertyString(key), true);
+ } catch (Exception ex) {
+ mLogger.logInfo(name + ": not defined", true);
+ }
+ }
+
+ int getResourceRatingTier() {
+ String key = mContext.getString(R.string.key_resource_rating);
+ try {
+ String value = mDrm.getPropertyString(key);
+ return Integer.parseInt(value);
+ } catch (Exception ex) {
+ mLogger.logInfo(key + ": property not available", true);
+ return 0;
+ }
+ }
+
+ // Fetch and install the license. If provisioning is needed, provision and
+ // then try to fetch and install again.
+ void getLicense() {
+ if (!checkInternet()) return;
+ try {
+ if (mDrm == null) {
+ init();
+ }
+ mSession = mDrm.openSession();
+ mLogger.setStatus("Generating license request");
+ byte[] initData = FDPT_PSSH;
+ String mimeType = "cenc";
+ String url = mServerURL + "?" + VIDEO_ID;
+ final MediaDrm.KeyRequest request =
+ mDrm.getKeyRequest(
+ mSession, initData, mimeType, MediaDrm.KEY_TYPE_STREAMING, null);
+
+ if (request == null) {
+ mLogger.setError("Key request not generated.");
+ return;
+ }
+ mLogger.setStatus("Requesting license");
+ Post post = new Post(url, request.getData());
+ Post.Response response = post.send();
+ byte[] data = parseResponseBody(response.body);
+ mKeySet = mDrm.provideKeyResponse(mSession, data);
+ if (mKeySet == null) {
+ mLogger.setError("Empty key set.");
+ return;
+ }
+ mLogger.setStatus("License installed");
+ mHasLicense = true;
+ } catch (NotProvisionedException e) {
+ if (fetchProvisioning()) {
+ // Once provisioned, start over.
+ getLicense();
+ }
+ } catch (ResourceBusyException e) {
+ mLogger.logInfo("Could not open session: " + e, true);
+ mLogger.setError("Failed to open session");
+ return;
+ } catch (DeniedByServerException e) {
+ mLogger.logInfo("License Denied: " + e, true);
+ mLogger.setError("License denied");
+ return;
+ } catch (SecurityException e) {
+ // This happens if the app does not have permission to connect to the
+ // internet.
+ mLogger.logInfo("Permission failure: " + e, true);
+ mLogger.setError("security exception: no internet permission?");
+ return;
+ } catch (IOException e) {
+ mLogger.logInfo("Could not get license: " + e, true);
+ mLogger.setError("Failed to get license");
+ return;
+ }
+ }
+
+ private byte[] parseResponseBody(byte[] responseBody) throws IOException {
+ final String bodyString = new String(responseBody, "UTF-8");
+
+ if (!bodyString.startsWith("GLS/")) {
+ return responseBody;
+ }
+ if (!bodyString.startsWith("GLS/1.")) {
+ throw new IOException("Invalid server version, expected 1.x");
+ }
+ final int drmMessageOffset = bodyString.indexOf("\r\n\r\n");
+
+ if (drmMessageOffset == -1) {
+ throw new IOException("Invalid server response, could not locate drm message");
+ }
+ return Arrays.copyOfRange(responseBody, drmMessageOffset + 4, responseBody.length);
+ }
+
+ private boolean fetchProvisioning() {
+ try {
+ mLogger.setStatus("Fetching provisioning");
+ final MediaDrm.ProvisionRequest request = mDrm.getProvisionRequest();
+ final byte[] data = request.getData();
+ final String signedUrl =
+ String.format("%s&signedRequest=%s", request.getDefaultUrl(), new String(data));
+ final Post post = new Post(signedUrl, null);
+ final Post.Response response = post.send();
+ mDrm.provideProvisionResponse(response.body);
+ mLogger.setStatus("Finished provisioning");
+ return true;
+ } catch (SecurityException e) {
+ // This happens if the app does not have permission to connect to the
+ // internet.
+ mLogger.logInfo("Permission failure: " + e, true);
+ mLogger.setError("security exception: no internet permission?");
+ return false;
+ } catch (Exception ex) {
+ mLogger.logInfo("Could not provision: " + ex, true);
+ mLogger.setError("failed to provision");
+ return false;
+ }
+ }
+
+ boolean hasLicense() {
+ return mHasLicense;
+ }
+
+ private boolean checkInternet() {
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ Network network = connectivityManager.getActiveNetwork();
+ if (network == null) {
+ mLogger.setError("No network connected.");
+ return false;
+ }
+ return true;
+ }
+
+ boolean sendHash(byte[] hash, TestCase test, IWorker worker) {
+ if (mDrm == null || mSession == null) {
+ mLogger.setError("Session not open");
+ return false;
+ }
+ try {
+ String sSession = new String(mSession);
+ String value =
+ sSession
+ + ","
+ + test.getFrameNumber()
+ + ","
+ + HexUtil.dumpHexBytes(hash, 0, hash.length);
+ mDrm.setPropertyString("decryptHashSessionId", sSession);
+ mDrm.setPropertyString("decryptHash", value);
+ } catch (Exception ex) {
+ worker.handleException("setting hash", ex);
+ return false;
+ }
+ return true;
+ }
+
+ boolean foundError(TestCaseFactory factory, IWorker worker) {
+ try {
+ String result = mDrm.getPropertyString("decryptHashError");
+ String[] csv = result.split(",");
+ if (csv.length != 2) {
+ mLogger.setError("Confusing hash error: " + result);
+ return true;
+ }
+ String code = csv[0];
+ if (code.equals("0")) { // OEMCrypto_SUCCESS.
+ return false;
+ } else if (code.equals("53")) { // OEMCrypto_BAD_HASH
+ int frameNumber = Integer.parseInt(csv[1]);
+ int index = frameNumber - 1;
+ TestCase test = factory.getTest(index);
+ mLogger.setError(test.getDescription() + " hash wrong");
+ test.logInfo(mLogger);
+ return true;
+ } else {
+ mLogger.setError("Confusing hash code: " + result);
+ }
+ } catch (Exception ex) {
+ worker.handleException("checking hash error", ex);
+ return true;
+ }
+ return false;
+ }
+
+ final byte[] getTestKey() {
+ return TEST_KEY_DATA;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Logger.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Logger.java
new file mode 100644
index 00000000..12139a7a
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Logger.java
@@ -0,0 +1,145 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.app.Activity;
+import android.text.Html;
+import android.util.Log;
+import android.widget.TextView;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+/**
+ * Logs information to logcat and also important information and errors to a status view in the UI.
+ */
+class Logger {
+ private static final String TAG = "FDPT";
+
+ // The activity to which we post UI tasks.
+ private Activity mActivity;
+
+ // The text view used for logging.
+ private TextView mLogView;
+ private TextView mStatusView;
+ private StringBuffer mLogBuilder;
+ private boolean mNewLogLine = true;
+
+ private String mStatusString = null;
+ private String mErrorString = null;
+ private int mFrame = -1;
+ private int mMaximumFrame = 0;
+
+ Logger() {
+ mLogBuilder = new StringBuffer();
+ }
+
+ void setActivity(Activity activity) {
+ this.mActivity = activity;
+ this.mLogView = (TextView) mActivity.findViewById(R.id.log_output);
+ this.mStatusView = (TextView) mActivity.findViewById(R.id.status);
+ }
+
+ // Only call from UI thread.
+ void clearLog() {
+ mLogBuilder = new StringBuffer();
+ if (mLogView != null) mLogView.setText("");
+ }
+
+ // Log information to the log view and to logcat.
+ void logInfo(final String s, final boolean end_of_line) {
+ Log.i(TAG, s);
+ if (mActivity != null) {
+ mActivity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mNewLogLine) {
+ String time_stamp =
+ new SimpleDateFormat("MM-dd HH:mm:ss ")
+ .format(Calendar.getInstance().getTime());
+ mLogBuilder.append(time_stamp);
+ }
+ mLogBuilder.append(s);
+ if (end_of_line) {
+ mLogBuilder.append("
");
+ mNewLogLine = true;
+ } else {
+ mLogBuilder.append(", ");
+ mNewLogLine = false;
+ }
+ mLogView.setText(Html.fromHtml(mLogBuilder.toString()));
+ }
+ });
+ }
+ }
+
+ void logException(final String s, Exception ex) {
+ setError(s + " " + ex.getMessage());
+ Log.e(TAG, s, ex);
+ }
+
+ // Log information to the status view, the log view, and to logcat.
+ // Run on the worker thread.
+ synchronized void setStatus(String status) {
+ mStatusString = status;
+ logInfo("Status Changed: " + mStatusString, true);
+ updateStatus();
+ }
+
+ // Update the current frame number in the counter view.
+ // Run on the worker thread.
+ synchronized void setStatus(int currentFrame, int maximumFrame) {
+ mFrame = currentFrame;
+ mMaximumFrame = maximumFrame;
+ Log.i(TAG, "Frame " + (currentFrame + 1) + "/" + maximumFrame);
+ updateStatus();
+ }
+
+ // Log information to the status view, the log view, and to logcat. An error
+ // will not be replaced by a new status -- only by new errors.
+ // Run on the worker thread.
+ synchronized void setError(String errorString) {
+ if (mErrorString == null) mErrorString = errorString;
+ logInfo("Error: " + mErrorString, true);
+ updateStatus();
+ }
+
+ synchronized String getErrorString() {
+ return mErrorString;
+ }
+
+ // This called by the worker to indicate a new test is starting.
+ synchronized void clearStatus() {
+ mFrame = -1;
+ mMaximumFrame = 0;
+ mErrorString = null;
+ mStatusString = "";
+ updateStatus();
+ }
+
+ synchronized void updateStatus() {
+ // TODO: don't flood the ui thread with frame number updates.
+ String status;
+ if (mErrorString != null) {
+ status = "" + mErrorString + "";
+ } else {
+ status = mStatusString;
+ }
+ if (mMaximumFrame > 0) {
+ // It looks better to start counting at 1.
+ status += " Frame " + (mFrame + 1) + "/" + mMaximumFrame + ".";
+ }
+ final String newStatus = status;
+ if (mActivity != null) {
+ mActivity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ mStatusView.setText(Html.fromHtml(newStatus));
+ }
+ });
+ }
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/MainActivity.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/MainActivity.java
new file mode 100644
index 00000000..3d256864
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/MainActivity.java
@@ -0,0 +1,121 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.SurfaceView;
+import android.view.View;
+
+/** This is the main view for the application on phones and tablets. */
+public class MainActivity extends Activity {
+
+ // Used to load the 'native-lib' library on application startup.
+ static {
+ System.loadLibrary("native-lib");
+ }
+
+ private Logger mLogger;
+ private Worker mWorker;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ setTitle(R.string.short_app_name);
+
+ FDPTApplication app = (FDPTApplication) getApplicationContext();
+ app.setActivity(this);
+ mLogger = app.getLogger();
+ mWorker = app.getWorker();
+
+ // Note: If you are starting things in batch mode, you might also want to edit
+ // preferences in batch mode. Use this:
+ // DIR=/data/data/com.google.widevine.fulldecryptpathtesting/shared_prefs
+ // FILE=com.google.widevine.fulldecryptpathtesting_preferences.xml
+ // adb pull $DIR/$FILE
+ // Then edit $FILE and then
+ // adb push $FILE $DIR/$FILE
+
+ Intent intent = getIntent();
+ String data = intent.getDataString();
+ if ("start".equals(intent.getDataString())) {
+ // For batch mode, use this command to start the tests:
+ // adb shell am start -n "com.google.widevine.fulldecryptpathtesting/.MainActivity" \
+ // -d "start"
+ startTest(null);
+ }
+ if ("stop".equals(intent.getDataString())) {
+ // For batch mode, use this command to stop the tests:
+ // adb shell am start -n "com.google.widevine.fulldecryptpathtesting/.MainActivity" \
+ // -d "stop"
+ stopTest(null);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mWorker.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mWorker.onResume();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onStop();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.action_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.menu_setup) {
+ configureTest(null);
+ }
+ if (item.getItemId() == R.id.menu_play) {
+ startTest(null);
+ }
+ return true;
+ }
+
+ // Called when Setup button is pressed.
+ public void configureTest(View view) {
+ startActivity(new Intent(this, SettingsActivity.class));
+ }
+
+ // Called when start test button is pressed.
+ public void startTest(View view) {
+ SurfaceView surfaceView = (SurfaceView) findViewById(R.id.playback_view);
+ Context context = getBaseContext();
+ TestParameters parameters = new TestParameters(context, mLogger);
+ TestRunner runner = new TestRunner(mLogger, context, surfaceView, parameters);
+ mWorker.startTest(runner);
+ }
+
+ // Called when stop test button is pressed.
+ public void stopTest(View view) {
+ mWorker.cancelTest();
+ }
+
+ // Called when clear button is pressed.
+ public void clearLog(View view) {
+ mLogger.clearLog();
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/MediaUtil.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/MediaUtil.java
new file mode 100644
index 00000000..9950c0e3
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/MediaUtil.java
@@ -0,0 +1,38 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class MediaUtil {
+ private MediaUtil() {}
+
+ public static String getCodecNameForMime(String mime, boolean secure) {
+ MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS);
+ mcl.getCodecInfos();
+ for (MediaCodecInfo info : mcl.getCodecInfos()) {
+ if (info.isEncoder()) {
+ continue;
+ }
+ List supportedTypes = Arrays.asList(info.getSupportedTypes());
+ if (!supportedTypes.contains(mime)) {
+ continue;
+ }
+ if (secure) {
+ CodecCapabilities caps = info.getCapabilitiesForType(mime);
+ if (caps.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback)) {
+ return info.getName();
+ }
+ } else {
+ return info.getName();
+ }
+ }
+ return null;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Post.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Post.java
new file mode 100644
index 00000000..a989a9aa
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Post.java
@@ -0,0 +1,189 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.util.Base64;
+import android.util.Log;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Post a request to a license or provisioning server and wait for the response. */
+public final class Post {
+
+ private static final int TIMEOUT_MS = 5000;
+ private static final int MAX_TRIES = 5;
+ private static final String TAG = "WVPostRequest";
+
+ static final class Response {
+ final int code;
+ final byte[] body;
+
+ Response(int code, byte[] body) {
+ this.code = code;
+ this.body = body;
+ }
+ }
+
+ private static final byte[] EMPTY_BODY = new byte[0];
+
+ private final String mUrl;
+ private final byte[] mData;
+ private final boolean mExpectOutput;
+
+ private final Map mProperties = new HashMap<>();
+
+ Post(String url, byte[] data) {
+ mUrl = url;
+ mData = data == null ? EMPTY_BODY : Arrays.copyOf(data, data.length);
+ mExpectOutput = data != null;
+ setProperty("Accept", "*/*");
+ setProperty("User-Agent", "Widevine Full Decrypt Path Test Application");
+ setProperty("Connection", "close");
+ if (data != null) setProperty("Content-Type", "application/json");
+ }
+
+ void setProperty(String key, String value) {
+ mProperties.put(key, value);
+ }
+
+ Response send() throws IOException {
+
+ int tries = 1;
+ boolean needRetry = true;
+ Response response = null;
+
+ while (needRetry) {
+ HttpURLConnection connection = null;
+ needRetry = false;
+
+ try {
+
+ connection = (HttpURLConnection) new URL(mUrl).openConnection();
+
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(mExpectOutput);
+ connection.setDoInput(true);
+ connection.setConnectTimeout(TIMEOUT_MS);
+ connection.setReadTimeout(TIMEOUT_MS);
+
+ for (final Map.Entry property : mProperties.entrySet()) {
+ connection.setRequestProperty(property.getKey(), property.getValue());
+ }
+
+ try (final OutputStream out = connection.getOutputStream()) {
+ out.write(mData);
+ }
+
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ int connectionResponse = connection.getResponseCode();
+
+ if (connectionResponse < 400) {
+ try (final InputStream inputStream = connection.getInputStream()) {
+ connectStreams(inputStream, outputStream);
+ }
+ } else {
+ try (final InputStream inputStream = connection.getErrorStream()) {
+ connectStreams(inputStream, outputStream);
+ }
+ }
+
+ response = new Response(connection.getResponseCode(), outputStream.toByteArray());
+
+ // Logging license request / responses
+ // Catching in try/catch in case other type of request / response that isn't
+ // loggable
+ // Provisioning has an empty request and we have no way to decode the
+ // response at present, so removing it from request / response logging
+ try {
+ if (!mUrl.contains("provisioning")) {
+ String myRequest = Base64.encodeToString(mData, Base64.NO_WRAP);
+ Log.i("LICENSE_REQUEST:", myRequest);
+ Log.i("FLUSH", "flushing");
+ }
+ } catch (Exception ex) {
+ Log.e(
+ "LICENSE_REQUEST",
+ "Failure to log licensing request in videoplayer Post.",
+ ex);
+ }
+
+ try {
+ if (!mUrl.contains("provisioning")) {
+ String myResponse =
+ Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP);
+ myResponse = myResponse.equals("") ? "EMPTY" : myResponse;
+ if (connectionResponse >= 400) {
+ String s = new String(outputStream.toByteArray(), "UTF-8");
+ Log.i("LICENSE_RESPONSE", s);
+ } else {
+ Log.i("LICENSE_RESPONSE", myResponse);
+ }
+ Log.i("FLUSH", "flushing");
+ }
+ } catch (Exception ex) {
+ Log.e(
+ "LICENSE_RESPONSE",
+ "Failure to log licensing response in videoplayer Post.",
+ ex);
+ }
+
+ } catch (SocketTimeoutException ste) {
+
+ if (tries == MAX_TRIES) {
+ throw ste;
+ }
+
+ Log.w(TAG, "Retrying after receiving SocketTimeoutException on try " + tries);
+ tries++;
+ needRetry = true;
+
+ } catch (Exception ex) {
+ Log.e(TAG, "Unexpected failure in response / request.", ex);
+
+ } finally {
+
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+ if (response == null) {
+ throw new IOException("Empty response");
+ }
+ if (response.code != 200) {
+ throw new IOException("Server returned HTTP error code " + response.code);
+ }
+ if (response.body == null) {
+ throw new IOException("No response from server");
+ }
+ if (response.body.length == 0) {
+ throw new IOException("Empty response from server");
+ }
+ return response;
+ }
+
+ private static void connectStreams(InputStream in, OutputStream out) throws IOException {
+
+ final byte scratch[] = new byte[1024];
+
+ int read; /* declare this here so that the for loop can be aligned */
+
+ for (read = in.read(scratch, 0, scratch.length);
+ read != -1;
+ read = in.read(scratch, 0, scratch.length)) {
+
+ out.write(scratch, 0, read);
+ }
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/SettingsActivity.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/SettingsActivity.java
new file mode 100644
index 00000000..c463ed10
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/SettingsActivity.java
@@ -0,0 +1,33 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceFragment;
+
+// This is the UI for the settings or preferences.
+// TODO: when the user changes L1/L3, pick a different default codec.
+// TODO: pick a good default codec.
+public class SettingsActivity extends PreferenceActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getFragmentManager()
+ .beginTransaction()
+ .replace(android.R.id.content, new MySettingsFragment())
+ .commit();
+ }
+
+ public static class MySettingsFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Read the list of preferences that the user can change from the file
+ // mobile/src/main/res/xml/preferences.xml.
+ addPreferencesFromResource(R.xml.preferences);
+ }
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestCase.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestCase.java
new file mode 100644
index 00000000..bbe4c765
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestCase.java
@@ -0,0 +1,96 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.media.MediaCodec;
+
+// A single test case.
+class TestCase {
+ private String mDescription;
+
+ // The size of the frame buffer.
+ private int mBufferSize;
+
+ // The frame number. Used for documenting problems.
+ private int mFrameNumber;
+
+ private MediaCodec.CryptoInfo.Pattern mPattern;
+
+ // The encryption pattern and mode.
+ private MediaCodec.CryptoInfo mCryptoInfo;
+
+ TestCase(
+ int frameNumber,
+ String description,
+ int bufferSize,
+ MediaCodec.CryptoInfo cryptoInfo,
+ MediaCodec.CryptoInfo.Pattern pattern) {
+ this.mDescription = description;
+ this.mBufferSize = bufferSize;
+ this.mPattern = pattern;
+ this.mCryptoInfo = cryptoInfo;
+ this.mFrameNumber = frameNumber;
+ }
+
+ public String toString() {
+ return "Frame: "
+ + mFrameNumber
+ + ", Buffer size: "
+ + mBufferSize
+ + ", Pattern = "
+ + mPattern
+ + ", Crypto: "
+ + mCryptoInfo;
+ }
+
+ public void logInfo(Logger logger) {
+ logger.logInfo("Frame " + mFrameNumber + ": " + mDescription, true);
+ StringBuilder sizes = new StringBuilder("size = [");
+ for (int i = 0; i < mCryptoInfo.numSubSamples; i++) {
+ sizes.append(" (");
+ sizes.append(mCryptoInfo.numBytesOfClearData[i]);
+ sizes.append(", ");
+ sizes.append(mCryptoInfo.numBytesOfEncryptedData[i]);
+ sizes.append(");");
+ }
+ sizes.append("] total=");
+ sizes.append(mBufferSize);
+ logger.logInfo(sizes.toString(), true);
+ String mode =
+ (mCryptoInfo.mode == MediaCodec.CRYPTO_MODE_AES_CTR) ? "Mode = CTR" : "Mode = CBC";
+ logger.logInfo(
+ mode
+ + ", Pattern = (e="
+ + mPattern.getEncryptBlocks()
+ + ", s="
+ + mPattern.getSkipBlocks()
+ + ")",
+ true);
+ }
+
+ final MediaCodec.CryptoInfo getCryptoInfo() {
+ return mCryptoInfo;
+ }
+
+ final int getFrameNumber() {
+ return mFrameNumber;
+ }
+
+ final int getBufferSize() {
+ return mBufferSize;
+ }
+
+ final int getEncryptBlocks() {
+ return mPattern.getEncryptBlocks();
+ }
+
+ final int getSkipBlocks() {
+ return mPattern.getSkipBlocks();
+ }
+
+ final String getDescription() {
+ return mDescription;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestCaseFactory.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestCaseFactory.java
new file mode 100644
index 00000000..c2501ec3
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestCaseFactory.java
@@ -0,0 +1,404 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.media.MediaCodec;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Random;
+
+// Generates a list of test cases. It uses preferences to pick the test cases.
+public class TestCaseFactory {
+
+ private static final String TAG = "FDPT_TestCaseFactory";
+
+ private Logger mLogger;
+ private Random mRandom;
+ private ArrayList mTests = new ArrayList();
+ private int mMinSize;
+ private TestParameters mParameters;
+ private int mRRTier;
+
+ static final int AES_BLOCK_SIZE = 16;
+
+ TestCaseFactory(Logger logger, int minSize, int resourceRatingTier, TestParameters parameters) {
+ this.mLogger = logger;
+ this.mMinSize = minSize;
+ this.mParameters = parameters;
+ this.mRRTier = resourceRatingTier;
+
+ if (minSize <= 0) {
+ mLogger.setError("Frame Generator failed.");
+ return;
+ }
+
+ int seed = mParameters.getRandomSeed();
+ if (seed == 0) {
+ SecureRandom starter = new SecureRandom();
+ seed = starter.nextInt();
+ }
+ mLogger.logInfo("Picking random seed " + seed, true);
+ mRandom = new Random(seed);
+
+ if (mParameters.getTestCENC()) buildCENCTests();
+ if (mParameters.getTestCENS()) buildCENSTests();
+ if (mParameters.getTestCBC1()) buildCBC1Tests();
+ if (mParameters.getTestCBCS()) buildCBCSTests();
+ }
+
+ // The maximum number of subsamples required for the given resource rating tier.
+ int maxSubsamples() {
+ switch (mRRTier) {
+ default:
+ case 1:
+ return 10;
+ case 2:
+ return 16;
+ case 3:
+ return 32;
+ }
+ }
+
+ // The maximum sample size required for the given resource rating tier.
+ int maxSampleSize() {
+ int MB = 1024 * 1024;
+ switch (mRRTier) {
+ default:
+ case 1:
+ return 1 * MB;
+ case 2:
+ return 2 * MB;
+ case 3:
+ return 4 * MB;
+ }
+ }
+
+ // The maximum subsample size required for the given resource rating tier.
+ int maxSubsampleSize() {
+ int KB = 1024;
+ int MB = 1024 * 1024;
+ switch (mRRTier) {
+ default:
+ case 1:
+ return 100 * KB;
+ case 2:
+ return 500 * KB;
+ case 3:
+ return 1 * MB;
+ }
+ }
+
+ // Round up the given number to a multiple of pad.
+ final int roundUp(int originalSize, int pad) {
+ if (0 == pad) {
+ pad = AES_BLOCK_SIZE;
+ }
+ if ((originalSize % pad) > 0) {
+ return (originalSize + (pad - (originalSize % pad)));
+ } else {
+ return originalSize;
+ }
+ }
+
+ // Generate a random integer between 0 and max.
+ final int randomInt(int max) {
+ int x = mRandom.nextInt();
+ if (x < 0) x = -x;
+ return x % max;
+ }
+
+ // Decide if a pattern and mode combination should be tested or not and return true if the test
+ // should be skipped.
+ boolean forbidsPartialBlocks(MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ // By the CENC spec ISO/IEC 23001-7:2016, in sections 9.5.2.3 and 9.5.2.5, some modes and
+ // patterns require subsamples to end on an AES block boundary.
+ // (Thanks, Juce for reading the spec and figuring out which tests we can dump!)
+ // For "cens", which is CTR with a pattern, subsamples should end on a block boundary,
+ // so we throw out those tests. It is not worth testing the other case. The CENC spec
+ // says applications may prohibit these.
+ if (mode == MediaCodec.CRYPTO_MODE_AES_CTR && pattern.getEncryptBlocks() > 0) {
+ return true;
+ }
+ // Similarly, in "cbc1", subsamples SHALL end on a block boundary, so we throw out
+ // all those tests.
+ if (mode == MediaCodec.CRYPTO_MODE_AES_CBC && pattern.getEncryptBlocks() == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ private void buildCENCTests() {
+ String description = "cenc";
+ MediaCodec.CryptoInfo.Pattern pattern = new MediaCodec.CryptoInfo.Pattern(0, 0);
+ int mode = MediaCodec.CRYPTO_MODE_AES_CTR;
+ buildTestsForMode(description, pattern, mode);
+ if (mParameters.getTestWrap()) {
+ buildWrappingIVTest(description, pattern, mode);
+ }
+ }
+
+ private void buildCENSTests() {
+ String description = "cens";
+ MediaCodec.CryptoInfo.Pattern pattern = new MediaCodec.CryptoInfo.Pattern(1, 9);
+ int mode = MediaCodec.CRYPTO_MODE_AES_CTR;
+ buildEncTests(description, pattern, mode);
+ buildTestsForModeRandomSizes(description, pattern, mode);
+ if (mParameters.getTestWrap()) {
+ buildWrappingIVTest(description, pattern, mode);
+ }
+ }
+
+ private void buildCBC1Tests() {
+ String description = "cbc1";
+ MediaCodec.CryptoInfo.Pattern pattern = new MediaCodec.CryptoInfo.Pattern(0, 0);
+ int mode = MediaCodec.CRYPTO_MODE_AES_CBC;
+ buildEncTests(description, pattern, mode);
+ buildTestsForModeRandomSizes(description, pattern, mode);
+ }
+
+ private void buildCBCSTests() {
+ String description = "cbcs";
+ MediaCodec.CryptoInfo.Pattern pattern = new MediaCodec.CryptoInfo.Pattern(1, 9);
+ int mode = MediaCodec.CRYPTO_MODE_AES_CBC;
+ buildTestsForMode(description, pattern, mode);
+ }
+
+ // Build tests for the given mode and skip pattern by generating a variety of sizes.
+ private void buildTestsForMode(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ buildTestsForModeSpecialSizes(description, pattern, mode);
+ buildTestsForModeRandomSizes(description, pattern, mode);
+ }
+
+ // Build some tests that have edge cases as the subsample sizes. These tests are
+ // also in the OEMCrypto unit tests.
+ private void buildTestsForModeSpecialSizes(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+
+ // Note: The first test case has to have some encryption so that
+ // the CDM layer calls SelectKey before we try to compute a hash.
+ // That is because we don't necessarily compute a hash for CopyBuffer.
+ buildEncTests(description, pattern, mode);
+ buildClearTests(description, pattern, mode);
+
+ int[] pads = {0, 10, 25, 32};
+ for (int k = 0; k < pads.length; k++) {
+ buildNoOffsetTests(description, pattern, mode, pads[k]);
+ }
+ for (int k = 0; k < pads.length; k++) {
+ buildEvenOffsetTests(description, pattern, mode, pads[k]);
+ }
+ for (int k = 0; k < pads.length; k++) {
+ buildOddOffsetTests(description, pattern, mode, pads[k]);
+ }
+ buildPartialBlockTests(description, pattern, mode);
+ buildSmallSubsampleTests(description, pattern, mode);
+ if (mParameters.getTestMax()) {
+ buildMaxSampleTests(description, pattern, mode);
+ buildMaxSubSampleTests(description, pattern, mode);
+ }
+ }
+
+ // This tests the ability to decrypt multiple subsamples with no offset.
+ // There is no offset within the block, used by CTR mode. However, there might
+ // be an offset in the encrypt/skip pattern.
+ private void buildNoOffsetTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode, int pad) {
+ int[] clearSizes = {pad, pad, pad};
+ int[] encryptedSizes = {48, 64, roundUp(mMinSize, 16)};
+ buildTestsForModeAndSizes(
+ description + ", no offset", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // This tests an offset into the block for the second encrypted subsample.
+ // This should only work for CTR mode, for CBC mode an error is expected in
+ // the decrypt step.
+ // If this test fails for CTR mode, then it is probably handling the
+ // block_offset incorrectly.
+ private void buildEvenOffsetTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode, int pad) {
+ int[] clearSizes = {pad, pad, pad};
+ int[] encryptedSizes = {16 + 8, 32, roundUp(mMinSize, 16)};
+ buildTestsForModeAndSizes(
+ description + ", even offset", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // If the EvenOffset test passes, but this one doesn't, then DecryptCTR might
+ // be using the wrong definition of block offset. Adding the block offset to
+ // the block boundary should give you the beginning of the encrypted data.
+ // This should only work for CTR mode, for CBC mode, the block offset must be
+ // 0, so an error is expected in the decrypt step.
+ // Another way to view the block offset is with the formula:
+ // block_boundary + block_offset = beginning of subsample.
+ private void buildOddOffsetTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode, int pad) {
+ int[] clearSizes = {pad, pad, pad};
+ int[] encryptedSizes = {50, 75, roundUp(mMinSize, 16)};
+ buildTestsForModeAndSizes(
+ description + ", odd offset", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // This tests the case where an encrypted sample is not an even number of
+ // blocks. For CTR mode, the partial block is encrypted. For CBC mode the
+ // partial block should be a copy of the clear data.
+ private void buildPartialBlockTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ int[] clearSizes = {mMinSize};
+ int[] encryptedSizes = {50};
+ buildTestsForModeAndSizes(
+ description + ", partial block", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // A small subsample.
+ private void buildSmallSubsampleTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ int[] clearSizes = {5, mMinSize};
+ int[] encryptedSizes = {5, 0};
+ buildTestsForModeAndSizes(
+ description + ", small subsample", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // The whole sample is clear.
+ private void buildClearTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ int[] clearSizes = {mMinSize};
+ int[] encryptedSizes = {0};
+ buildTestsForModeAndSizes(
+ description + ", all clear", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // The whole sample is encrypted.
+ private void buildEncTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ int[] clearSizes = {0};
+ int[] encryptedSizes = {roundUp(mMinSize, 16)};
+ buildTestsForModeAndSizes(
+ description + ", all encrypted", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // A test with maximum sample size.
+ private void buildMaxSampleTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ int subsize = maxSampleSize() / maxSubsamples();
+ int[] clearSizes = new int[maxSubsamples()];
+ int[] encryptedSizes = new int[maxSubsamples()];
+ for (int i = 0; i < maxSubsamples(); i++) {
+ clearSizes[i] = 0;
+ encryptedSizes[i] = subsize;
+ }
+ buildTestsForModeAndSizes(
+ description + ", max sample", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // A test with maximum subsample size.
+ private void buildMaxSubSampleTests(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ int count = maxSampleSize() / maxSubsampleSize();
+ int[] clearSizes = new int[count];
+ int[] encryptedSizes = new int[count];
+ for (int i = 0; i < count; i++) {
+ clearSizes[i] = 0;
+ encryptedSizes[i] = maxSubsampleSize();
+ }
+ buildTestsForModeAndSizes(
+ description + ", max subsample", pattern, mode, clearSizes, encryptedSizes);
+ }
+
+ // Build a bunch of randomly sized buffers for testing different sizes. The
+ // user can select how many tests to run in the settings screen.
+ private void buildTestsForModeRandomSizes(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ int count = mParameters.getRandomCount();
+ boolean doRounding = forbidsPartialBlocks(pattern, mode);
+ for (int t = 0; t < count; t++) {
+ int numSubsamples = 1 + randomInt(maxSubsamples());
+ if (doRounding && !mParameters.getTestMultiSubsample()) {
+ // For the modes that require subsamples end on a block boundary,
+ // multisubsamples is rarely used. We don't run those tests
+ // unless explicitly requested.
+ numSubsamples = 1;
+ }
+ // Split the smallest into even pieces, and round up.
+ int base_size = mMinSize / numSubsamples + 1;
+ int[] clearSizes = new int[numSubsamples];
+ int[] encryptedSizes = new int[numSubsamples];
+ for (int i = 0; i < numSubsamples; i++) {
+ clearSizes[i] = randomInt(100);
+ encryptedSizes[i] = base_size + randomInt(600);
+ if (doRounding) {
+ encryptedSizes[i] = roundUp(encryptedSizes[i], AES_BLOCK_SIZE);
+ }
+ }
+ buildTestsForModeAndSizes(description, pattern, mode, clearSizes, encryptedSizes);
+ }
+ }
+
+ private boolean buildTestsForModeAndSizes(
+ String description,
+ MediaCodec.CryptoInfo.Pattern pattern,
+ int mode,
+ int[] clearSizes,
+ int[] encryptedSizes) {
+ // Throw out tests that are forbidden.
+ if (forbidsPartialBlocks(pattern, mode)) {
+ for (int i = 0; i < encryptedSizes.length; i++) {
+ if (encryptedSizes[i] % AES_BLOCK_SIZE != 0) return false;
+ }
+ }
+ // Test with a random iv.
+ byte[] iv = new byte[16];
+ mRandom.nextBytes(iv);
+ addTest(description, pattern, mode, clearSizes, encryptedSizes, iv);
+ return true;
+ }
+
+ // This is used to test that the IV wraps correctly in CTR mode.
+ private void buildWrappingIVTest(
+ String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
+ int[] clearSizes = {0};
+ int[] encryptedSizes = {roundUp(mMinSize, AES_BLOCK_SIZE)};
+ byte[] iv = new byte[16];
+ mRandom.nextBytes(iv);
+ // Set the last 8 bytes to be near the wrap value:
+ for (int i = 8; i < 16; i++) iv[i] = (byte) 0xFF;
+ iv[15] = (byte) 0xFE;
+ addTest(description + ", wrapping iv", pattern, mode, clearSizes, encryptedSizes, iv);
+ }
+
+ private void addTest(
+ String description,
+ MediaCodec.CryptoInfo.Pattern pattern,
+ int mode,
+ int[] clearSizes,
+ int[] encryptedSizes,
+ byte[] iv) {
+ // Start counting frame number at 1, for human readability.
+ int frameNumber = mTests.size() + 1;
+ if (clearSizes.length != encryptedSizes.length) {
+ mLogger.setError("Malformed Test " + frameNumber);
+ return;
+ }
+ int testFrameSize = 0;
+ for (int i = 0; i < clearSizes.length; i++) {
+ testFrameSize += clearSizes[i];
+ testFrameSize += encryptedSizes[i];
+ }
+ MediaCodec.CryptoInfo info = new MediaCodec.CryptoInfo();
+ info.set(
+ clearSizes.length, clearSizes, encryptedSizes, LicenseHolder.TEST_KEY_ID, iv, mode);
+ info.setPattern(pattern);
+ mTests.add(new TestCase(frameNumber, description, testFrameSize, info, pattern));
+ }
+
+ final int getTestCount() {
+ return mTests.size();
+ }
+
+ // Get the test case for the specified index. This can be called
+ // multiple times for each frame.
+ final TestCase getTest(int index) {
+ return mTests.get(index);
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestParameters.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestParameters.java
new file mode 100644
index 00000000..2649280e
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestParameters.java
@@ -0,0 +1,191 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+class TestParameters {
+ private String mCodecName;
+ private boolean mUseLevel1 = false;
+ private boolean mTestCENC = true;
+ private boolean mTestCENS = false;
+ private boolean mTestCBCS = false;
+ private boolean mTestCBC1 = false;
+ private boolean mTestMax = false;
+ private boolean mTestWrap = false;
+ // TODO(b/139257871): This defaults to false until iv incrementing is corrected for cbc1.
+ private boolean mTestMultiSubsample = false;
+ private boolean mLogEachFrame = false;
+ private int mRandomCount = 5;
+ private int mRandomSeed = 0;
+ private int mTestFrame = -1;
+
+ // Create a minimal set of tests.
+ TestParameters() {
+ mCodecName = "default";
+ }
+
+ TestParameters(Context context, Logger logger) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ mCodecName = getCodecName(context, preferences);
+ mUseLevel1 = preferences.getBoolean(context.getString(R.string.key_use_level_1), true);
+ mTestCENC = preferences.getBoolean(context.getString(R.string.key_test_cenc), true);
+ mTestCENS = preferences.getBoolean(context.getString(R.string.key_test_cens), true);
+ mTestCBC1 = preferences.getBoolean(context.getString(R.string.key_test_cbc1), true);
+ mTestCBCS = preferences.getBoolean(context.getString(R.string.key_test_cbcs), true);
+ mTestMax = preferences.getBoolean(context.getString(R.string.key_test_max), false);
+ mTestWrap = preferences.getBoolean(context.getString(R.string.key_test_wrap), true);
+ mTestMultiSubsample =
+ preferences.getBoolean(context.getString(R.string.key_test_multi_subsample), false);
+ mLogEachFrame =
+ preferences.getBoolean(context.getString(R.string.key_log_each_frame), false);
+ String value =
+ preferences.getString(context.getString(R.string.key_random_test_count), "5000");
+ try {
+ mRandomCount = Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ logger.logException("Number format for preference " + value, e);
+ mRandomCount = 1000;
+ }
+
+ value = preferences.getString(context.getString(R.string.key_random_seed), "0");
+ try {
+ mRandomSeed = Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ mRandomSeed = 0; // Use 0 if string is empty or malformed.
+ }
+ value = preferences.getString(context.getString(R.string.key_test_frame), "0");
+ try {
+ mTestFrame = Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ mTestFrame = 0; // Use 0 if string is empty or malformed.
+ }
+ }
+
+ private String getCodecName(Context context, SharedPreferences preferences) {
+ // The user can specify a particular codec on the settings screen. If
+ // they leave it as default. Just to be clear, the default value is
+ // "default" and means we should get the codec by type instead of by
+ // name.
+ String codecName =
+ preferences.getString(
+ context.getString(R.string.key_codec),
+ context.getString(R.string.default_codec));
+ if (codecName == null
+ || codecName.equals(context.getString(R.string.default_codec))
+ || "".equals(codecName)) {
+ boolean secureCodec =
+ preferences.getBoolean(context.getString(R.string.key_use_secure_buffer), true);
+ codecName = MediaUtil.getCodecNameForMime(CodecHandler.MIME_TYPE, secureCodec);
+ }
+ return codecName;
+ }
+
+ public String getCodecName() {
+ return mCodecName;
+ }
+
+ public void setCodecName(String codecName) {
+ this.mCodecName = mCodecName;
+ }
+
+ public boolean getUseLevel1() {
+ return mUseLevel1;
+ }
+
+ public void setUseLevel1(boolean useLevel1) {
+ this.mUseLevel1 = useLevel1;
+ }
+
+ public boolean getTestCENC() {
+ return mTestCENC;
+ }
+
+ public void setTestCENC(boolean testCENC) {
+ this.mTestCENC = testCENC;
+ }
+
+ public boolean getTestCENS() {
+ return mTestCENS;
+ }
+
+ public void setTestCENS(boolean testCENS) {
+ this.mTestCENS = testCENS;
+ }
+
+ public boolean getTestCBCS() {
+ return mTestCBCS;
+ }
+
+ public void setTestCBCS(boolean testCBCS) {
+ this.mTestCBCS = testCBCS;
+ }
+
+ public boolean getTestCBC1() {
+ return mTestCBC1;
+ }
+
+ public void setTestCBC1(boolean testCBC1) {
+ this.mTestCBC1 = testCBC1;
+ }
+
+ public boolean getTestMax() {
+ return mTestMax;
+ }
+
+ public void setTestMax(boolean testMax) {
+ this.mTestMax = testMax;
+ }
+
+ public boolean getTestWrap() {
+ return mTestWrap;
+ }
+
+ public void setTestWrap(boolean testWrap) {
+ this.mTestWrap = testWrap;
+ }
+
+ public boolean getTestMultiSubsample() {
+ return mTestMultiSubsample;
+ }
+
+ public void setTestMultiSubsample(boolean testMultiSubsample) {
+ this.mTestMultiSubsample = testMultiSubsample;
+ }
+
+ public boolean getLogEachFrame() {
+ return mLogEachFrame;
+ }
+
+ public void setLogEachFrame(boolean logEachFrame) {
+ this.mLogEachFrame = logEachFrame;
+ }
+
+ public int getRandomCount() {
+ return mRandomCount;
+ }
+
+ public void setRandomCount(int randomCount) {
+ this.mRandomCount = mRandomCount;
+ }
+
+ public int getRandomSeed() {
+ return mRandomSeed;
+ }
+
+ public void setRandomSeed(int randomCount) {
+ this.mRandomSeed = mRandomSeed;
+ }
+
+ public int getTestFrame() {
+ return mTestFrame;
+ }
+
+ public void setTestFrame(int randomCount) {
+ this.mTestFrame = mTestFrame;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestRunner.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestRunner.java
new file mode 100644
index 00000000..f28226fb
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/TestRunner.java
@@ -0,0 +1,158 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import android.content.Context;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.view.SurfaceView;
+
+// Helps initialize the other objects used in running the tests.
+public class TestRunner {
+
+ private static final String TAG = "FDPT_TestRunner";
+
+ private Logger mLogger;
+ private Context mContext;
+ private SurfaceView mView;
+
+ private LicenseHolder mLicenseHolder;
+ private CodecHandler mCodecHandler;
+ private TestParameters mParameters;
+
+ TestRunner(Logger logger, Context context, SurfaceView view, TestParameters parameters) {
+ this.mLogger = logger;
+ this.mContext = context;
+ this.mView = view;
+ this.mParameters = parameters;
+ }
+
+ void cleanUp() {
+ if (mCodecHandler != null) {
+ mCodecHandler.cleanUp();
+ }
+ if (mLicenseHolder != null) {
+ mLicenseHolder.cleanUp();
+ }
+ }
+
+ boolean initLicense() {
+ mLicenseHolder = new LicenseHolder(mLogger, mParameters.getUseLevel1(), mContext);
+ if (mLicenseHolder.shouldSkipTest()) return false;
+ mLicenseHolder.getLicense();
+ if (!mLicenseHolder.hasLicense()) return false;
+ return true;
+ }
+
+ boolean initCodecHandler(MediaFormat format, IWorker worker) {
+ MediaCrypto crypto = mLicenseHolder.getMediaCrypto();
+ if (crypto == null) return false;
+
+ mCodecHandler = new CodecHandler(mLogger, crypto, mView, format, worker);
+
+ mCodecHandler.createCodecByName(mParameters.getCodecName());
+ if (!mCodecHandler.start()) return false;
+ return true;
+ }
+
+ // Run by the worker thread to do one test.
+ void doTest(IWorker worker) throws InterruptedException {
+ if (worker.finishEarly()) return;
+ mLogger.clearStatus();
+ mLogger.setStatus("Start test.");
+
+ if (!initLicense()) return;
+
+ FrameGenerator frameGenerator = new FrameGenerator(mContext, mLogger);
+ if (frameGenerator == null) return;
+ frameGenerator.prepareKeyFrame();
+
+ HashGenerator hashGenerator = new HashGenerator();
+
+ Encryptor encryptor = new Encryptor(mLicenseHolder, mLogger);
+
+ if (!initCodecHandler(frameGenerator.getMediaFormat(), worker)) return;
+ mLogger.setStatus("Codec ready.");
+
+ int minSize = frameGenerator.getMinimumFrameSize();
+ if (minSize <= 0) return;
+ int tier = mLicenseHolder.getResourceRatingTier();
+ TestCaseFactory mTestCaseFactory =
+ new TestCaseFactory(
+ mLogger, minSize,
+ tier, mParameters);
+
+ int testCount = mTestCaseFactory.getTestCount();
+ mLogger.setStatus("Running");
+ String currentDescription = "init";
+ mLogger.setStatus(-1, testCount); // Indicate no tests have passed.
+ if (testCount < 1) {
+ mLogger.setStatus("No test cases selected.");
+ mLogger.logInfo("Go to Setup page and select tests before clicking 'Start Test'", true);
+ return;
+ }
+ for (int i = 0; i < testCount; i++) {
+ worker.maybePause();
+ if (worker.finishEarly()) return;
+ TestCase test = mTestCaseFactory.getTest(i);
+ if (!currentDescription.equals(test.getDescription())) {
+ currentDescription = test.getDescription();
+ mLogger.setStatus("Testing " + currentDescription);
+ }
+ if (mParameters.getLogEachFrame()) {
+ test.logInfo(mLogger);
+ }
+ ByteArrayFrameBuilder fb = new ByteArrayFrameBuilder(test.getBufferSize());
+ byte[] clearFrame = frameGenerator.buildtestFrame(fb);
+ if (clearFrame == null) {
+ mLogger.setError("Frame generation failed.");
+ test.logInfo(mLogger);
+ return;
+ }
+ if (clearFrame.length != test.getBufferSize()) {
+ mLogger.setError(
+ "Frame generation size = "
+ + clearFrame.length
+ + ", expected "
+ + test.getBufferSize());
+ test.logInfo(mLogger);
+ return;
+ }
+ byte[] hash = hashGenerator.getHash(clearFrame, test);
+ if (hash == null) {
+ mLogger.setError("Hash generation failed.");
+ test.logInfo(mLogger);
+ return;
+ }
+ if (!mLicenseHolder.sendHash(hash, test, worker)) return;
+ byte[] encryptedFrame = encryptor.encryptFrame(clearFrame, test);
+ if (encryptedFrame == null) {
+ mLogger.setError("Encryption failed.");
+ test.logInfo(mLogger);
+ return;
+ }
+ boolean lastFrame = (i + 1 == testCount);
+
+ if (!mCodecHandler.sendData(test, clearFrame, encryptedFrame, lastFrame)) {
+ test.logInfo(mLogger);
+ return;
+ }
+ mLogger.setStatus(i, testCount);
+ if (mLicenseHolder.foundError(mTestCaseFactory, worker)) {
+ worker.handleError(mLogger.getErrorString());
+ mLogger.logInfo("got hash error, frame " + test.getFrameNumber(), true);
+ return;
+ }
+ }
+ mLogger.setStatus("Finishing test.");
+ mCodecHandler.finishFrames();
+
+ if (mLicenseHolder.foundError(mTestCaseFactory, worker)) {
+ worker.handleError(mLogger.getErrorString());
+ return;
+ }
+ mLogger.setStatus("Test finished.");
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Worker.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Worker.java
new file mode 100644
index 00000000..0251173f
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/java/com/google/widevine/fulldecryptpathtesting/Worker.java
@@ -0,0 +1,131 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+// This is the main worker thread that runs each test case.
+class Worker extends Thread implements IWorker {
+ private static final String TAG = "FDPT_Worker";
+
+ private boolean mFinished = false; // True when the main application is done.
+ private boolean mPaused = true; // True if the main activity is not visible.
+ private boolean mRunTest = false; // True if a test is running or should start.
+
+ private TestRunner mTestRunner;
+ private Logger mLogger;
+
+ Worker(Logger logger) {
+ super("Worker Thread");
+ this.mLogger = logger;
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (!mFinished) {
+ if (shouldStartTest()) {
+ mTestRunner.doTest(this);
+ finishTest();
+ }
+ }
+ } catch (InterruptedException ex) {
+ mLogger.logInfo("worker interrupted: " + ex.getMessage(), true);
+ }
+ }
+
+ // This is called by the main activity when the application is not visible.
+ synchronized void onPause() {
+ mPaused = true;
+ notifyAll();
+ }
+
+ // This is called by the main activity when the application becomes visible.
+ synchronized void onResume() {
+ mPaused = false;
+ notifyAll();
+ }
+
+ // This is called by the main activity when the application closes.
+ synchronized void onStop() {
+ mFinished = true;
+ notifyAll();
+ }
+
+ // This is called by the UI thread to start running a test.
+ synchronized void startTest(TestRunner testRunner) {
+ if (mRunTest) {
+ mLogger.logInfo("Test already started.", true);
+ } else {
+ mRunTest = true;
+ mTestRunner = testRunner;
+ }
+ notifyAll();
+ }
+
+ // This is called by the UI thread to cancel a running test.
+ // Run by worker thread.
+ synchronized void cancelTest() {
+ if (!mRunTest) {
+ mLogger.logInfo("Test not running.", true);
+ } else {
+ mRunTest = false;
+ }
+ notifyAll();
+ }
+
+ // This is called by the worker to see if a test should be started.
+ // It blocks until a test is started or the application ends.
+ // Run by worker thread.
+ private synchronized boolean shouldStartTest() {
+ if (!mFinished && !mRunTest) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ mLogger.logInfo("Worker Thread interrupted.", true);
+ }
+ }
+ return !mFinished && mRunTest;
+ }
+
+ // Check to see if the app is finished or the test has been stopped.
+ // Run by worker thread.
+ @Override
+ public synchronized boolean finishEarly() {
+ if (mFinished || !mRunTest) {
+ mLogger.setStatus("Test aborted.");
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void handleException(String message, Exception e) {
+ mLogger.logException(message, e);
+ }
+
+ @Override
+ public void handleError(String message) {}
+
+ // This blocks until mPaused is false, or the test is stopped.
+ // Run by worker thread.
+ @Override
+ public synchronized void maybePause() {
+ // Also finish pausing when the app is done or the test has been stopped.
+ while (mPaused && !mFinished && mRunTest) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ mLogger.logInfo("Worker Thread interrupted.", true);
+ }
+ }
+ }
+
+ // Finish the test and clean up.
+ synchronized void finishTest() {
+ if (mTestRunner != null) {
+ mTestRunner.cleanUp();
+ }
+ mRunTest = false;
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/drawable-v24/ic_launcher_foreground.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..c7bd21db
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/drawable/ic_launcher_background.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..d5fccc53
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/layout/activity_main.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..c2b023db
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/layout/activity_main.xml
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/menu/action_menu.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/menu/action_menu.xml
new file mode 100644
index 00000000..7c1216c5
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/menu/action_menu.xml
@@ -0,0 +1,14 @@
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..1206973e
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..4a3a8a96
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..736d2a36
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..d15d440c
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..3a204b3f
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..fbef56e8
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..cead4063
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..4e32a104
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..9121bdff
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..fd1e8697
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..63e38bc2
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..307cccea
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..d36c3865
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..c82c3079
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..f6f14843
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/fdpt.mp4 b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/fdpt.mp4
new file mode 100644
index 00000000..98519102
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/fdpt.mp4 differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_bmp.bmp b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_bmp.bmp
new file mode 100644
index 00000000..8d2e9174
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_bmp.bmp differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_jpeg.jpeg b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_jpeg.jpeg
new file mode 100644
index 00000000..0089e88d
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_jpeg.jpeg differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_mpeg.mp4 b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_mpeg.mp4
new file mode 100644
index 00000000..dc6bc49d
Binary files /dev/null and b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/raw/logo_mpeg.mp4 differ
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/colors.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/colors.xml
new file mode 100644
index 00000000..3ab3e9cb
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/ic_launcher_background.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..be3873fc
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #402174
+
\ No newline at end of file
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/keys.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/keys.xml
new file mode 100644
index 00000000..b33c89aa
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/keys.xml
@@ -0,0 +1,28 @@
+
+
+
+ test_cenc
+ test_cens
+ test_cbc1
+ test_cbcs
+ test_max
+ test_wrap
+ test_multi_subsample
+ test_log_each_frame
+ random_test_count
+ random_seed
+ random_test_count
+ use_level_1
+ use_secure_buffer
+ codec
+ default
+
+ securityLevel
+ systemId
+ version
+ oemCryptoApiVersion
+ oemCryptoBuildInformation
+ decryptHashSupport
+ resourceRatingTier
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/strings.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/strings.xml
new file mode 100644
index 00000000..ea269c30
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/strings.xml
@@ -0,0 +1,89 @@
+
+ Full Decrypt Path Testing
+ FDPT
+ Setup
+ Start Test
+ Stop Test
+ Clear
+ Settings
+
+
+ Security Level
+ System ID
+ Widevine Plugin Version
+ OEMCrypto API version
+ OEMCrypto Build Info
+ Decrypt Hash Supported
+ Resource Rating Tier
+
+
+ Full Decrypt Path Testing Setup
+ Test \"cenc\"
+ Run tests with CTR mode and no pattern
+ Test \"cens\"
+ Run tests with CTR mode with pattern (rare)
+ Test \"cbc1\"
+ Run tests with CBC mode and no pattern (rare)
+ Test \"cbcs\"
+ Run tests with CBC mode with pattern
+ Test Maximum Buffer Sizes
+ Run tests with maximum sample
+ and subsample sizes for OEMCrypto. This may cause problems with the codec if it cannot
+ handle OEMCrypto\'s maximum buffer size.
+ Test IV overflow
+ Run tests with an IV in counter mode that
+ overflows when incremented.
+ Test Multisubsample for cens and cbc1
+ If true, tests are run which include
+ more than one subsample for \"cens\" and \"cbc1\". A bug in the CDM layer may
+ prevent \"cbc1\" test from passing.
+ Log Each Frame
+ If true, then a description of each
+ frame is logged to the output window. This is very verbose, and will slow
+ the tests down significantly. However, when debugging, it might help to see
+ descriptions of tests that pass.
+ Random Test Count
+ Click to set how many random tests
+ to run for each encryption scheme.
+ Random Seed
+ If set, this is used to seed the test
+ factory\'s random number generator. The seed may be found in the logcat. This
+ allows a user to repeate a previous test.
+ Test Frame
+ if set, only thiis frame will be tested.
+ This allows a user to isolate and debug a single failing test case.
+ Use Level 1
+ If this is set, the L1 OEMCrypto is used
+ Use Secure Buffer
+ If the default codec is used,
+ then this determines if it is secure or not.
+ Codec
+ The name of the Codec to use.
+ If this is not set, then a default codec is chosen for mime type video/avc.
+ The user must make sure it is for video and uses a secure buffer
+ if required.\n
+ current = %1$s
+ Use default codec for video mime type.
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/styles.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/styles.xml
new file mode 100644
index 00000000..5885930d
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/xml/preferences.xml b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/xml/preferences.xml
new file mode 100644
index 00000000..6bc514f1
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/main/res/xml/preferences.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/test/java/com/google/widevine/fulldecryptpathtesting/ExampleUnitTest.java b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/test/java/com/google/widevine/fulldecryptpathtesting/ExampleUnitTest.java
new file mode 100644
index 00000000..1172d93b
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/mobile/src/test/java/com/google/widevine/fulldecryptpathtesting/ExampleUnitTest.java
@@ -0,0 +1,21 @@
+// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
+// source code may only be used and distributed under the Widevine Master
+// License Agreement.
+
+package com.google.widevine.fulldecryptpathtesting;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
diff --git a/libwvdrmengine/test/java/FullDecryptPathTesting/settings.gradle b/libwvdrmengine/test/java/FullDecryptPathTesting/settings.gradle
new file mode 100644
index 00000000..6070d9b3
--- /dev/null
+++ b/libwvdrmengine/test/java/FullDecryptPathTesting/settings.gradle
@@ -0,0 +1 @@
+include ':mobile'